From 9ee2d3e7fd571bcd70dda35f0f43b2766d13f228 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 23 Apr 2019 23:15:34 +0100 Subject: [PATCH 001/134] Adding FAQ button to Settings --- .../sheet/SettingsOptionsBottomSheet.kt | 16 +++++++++++++++- base/src/main/res/drawable/icon_help.png | Bin 0 -> 1456 bytes base/src/main/res/values/strings.xml | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 base/src/main/res/drawable/icon_help.png diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index 6f297b62..a1966ad7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -1,6 +1,8 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog +import android.content.Intent +import android.net.Uri import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity @@ -125,6 +127,18 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { openSheet(activity, DeleteAndMoreOptionsBottomSheet()) } )) + options.add(LithoOptionsItem( + title = R.string.home_option_faq_title, + subtitle = R.string.home_option_faq_description, + icon = R.drawable.icon_help, + listener = { + try { + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_FAQ_URL))) + dismiss() + } catch (exception: Exception) { + } + } + )) options.add(LithoOptionsItem( title = R.string.home_option_logout_of_app, subtitle = R.string.home_option_logout_of_app_subtitle, @@ -139,7 +153,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { } companion object { - + const val GITHUB_FAQ_URL = "http://bijoysingh.github.io/Scarlet-Notes/faq/" const val KEY_MARKDOWN_ENABLED = "KEY_MARKDOWN_ENABLED" const val KEY_MARKDOWN_HOME_ENABLED = "KEY_MARKDOWN_HOME_ENABLED" diff --git a/base/src/main/res/drawable/icon_help.png b/base/src/main/res/drawable/icon_help.png new file mode 100644 index 0000000000000000000000000000000000000000..b6cb9b2311a7b2e564b4bd346e2a98602f463cf6 GIT binary patch literal 1456 zcmV;h1yA~kP)yA(~32K*=kzzE~%1Ta0^X1fM5G) z#!Yf^k%xDgLNuoE9(m+s;516J1sgU}hSMY~k%Q0Jflb@^oE(xhxQ^+>!gQ{a*yD8$ z5-SIJQDTXlRK(;clW0MHp5rzyB7sCM8h#>40w9?mSxaQgxm@hwJeLm& z)0E^WLy)FKwwCklO`Z%QSUGjm45#)I4EN#GP+V~qLokDpUXzLXINdM) zk(&^@VeY-hRcs@(R&$d_s6GT;D8QwM@6ctC;xY4A;m1bK@;Kbm(S7H?|5u|5dqq2p zz_01>T8JyYA&7$Fik9d;5kCl_+T*^NaX$NT3Q>@=MM&X(s0>=0k3f! z)dq5k3r=NU!1I7)4x_3eu6PUGui}Ej=$;T))IfD8lc76rpu1dL@ja?lb8&!e_k@^m z1XW&%3b|;A(?)I(Gv~o~L`>Lja|)50J~++iY{?YPM73Q^SY>k&(-o&hoFfQJHT(iL9o1oyC39$J zd|I^6{Iu9sGK+`Mm6t3=1#}NZ_q={u7We;dRO!+Fz|mZMK@;UE0-ySg<2RrRb8A*L zfSU=U+DLBkhab_Uladt;AcYKcKLmM22%#H7&R92qoD4%3BDeU*{Wvv>c>@~Zbf3*s zgJQ940L7zAHPCP-TXFg-Rt?}woVIbM1jo}jh50-d4d5%nI6WDggr}dyGJYhIh7v6j zm>u(!vov}W$!roNdYnNzk*w!2v4F=|Z#(Hv!}+dhmrbHhyKHPH?b3itJrZAk2$=Yq z#P=SFua{=;)Z?4&ryd)TzyQ1~r-Cv0JCVnz$a1_4AR%^@8_GTFZj%YZz*&s3r@g%8Lh zCl|NSjQ!cY6w!cG-Xr@LgPL)bqa61LTS#X{EXr}?;^N}s;(i03$C$OPg`n;L0000< KMNUMnLSTaXP`EY# literal 0 HcmV?d00001 diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3895083c..33d40ded 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -424,4 +424,7 @@ Allow notes in trash to be shown in home screen widgets + Help and Common Questions + Find help on how to use the features in the app + From b55be773db02fbc80cdc5526406006540c5f7cb9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 24 Apr 2019 22:21:25 +0100 Subject: [PATCH 002/134] Fix Markdown Issue mentioned in #129 --- .../maubis/markdown/inliners/InlineHtmlTextTests.kt | 6 +++--- .../maubis/markdown/inliners/InlineSimpleTextTests.kt | 11 +++++++++++ .../java/com/maubis/markdown/inliner/TextInliner.kt | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineHtmlTextTests.kt b/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineHtmlTextTests.kt index 41d62b9c..d8598755 100644 --- a/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineHtmlTextTests.kt +++ b/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineHtmlTextTests.kt @@ -14,7 +14,7 @@ class InlineHtmlTextTests : MarkdownTextInlinerTestBase() { assert(PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INVALID), listOf( NormalInlineMarkdownSegment("Hi"), PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.BOLD), listOf(NormalInlineMarkdownSegment("Hello"), - PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.UNDERLINE), listOf(NormalInlineMarkdownSegment("World"))))))), processed) + PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.ITALICS), listOf(NormalInlineMarkdownSegment("World"))))))), processed) } @Test @@ -22,9 +22,9 @@ class InlineHtmlTextTests : MarkdownTextInlinerTestBase() { val text = "HelloWorldItalicsStrong" val processed = TextInliner(text).get() assert(PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INVALID), listOf( - PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.ITALICS), listOf(NormalInlineMarkdownSegment("Hello"))), + PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.UNDERLINE), listOf(NormalInlineMarkdownSegment("Hello"))), PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INLINE_CODE), listOf(NormalInlineMarkdownSegment("World"))), - PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.UNDERLINE), listOf(NormalInlineMarkdownSegment("Italics"))), + PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.ITALICS), listOf(NormalInlineMarkdownSegment("Italics"))), PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.BOLD), listOf(NormalInlineMarkdownSegment("Strong"))))), processed) } diff --git a/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineSimpleTextTests.kt b/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineSimpleTextTests.kt index cf278864..03bf6d75 100644 --- a/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineSimpleTextTests.kt +++ b/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineSimpleTextTests.kt @@ -52,4 +52,15 @@ class InlineSimpleTextTests : MarkdownTextInlinerTestBase() { val processed = TextInliner(textToTest).get() assert(PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INLINE_CODE),listOf(NormalInlineMarkdownSegment(text))), processed) } + + @Test + fun testIncompleteText() { + val textA = "aaa_bb*c" + val processedA = TextInliner(textA).get() + assert(PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INVALID), listOf(NormalInlineMarkdownSegment(textA))), processedA) + + val textB = "aaa_" + val processedB = TextInliner(textB).get() + assert(PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INVALID), listOf(NormalInlineMarkdownSegment(textB))), processedB) + } } diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt index 13e7c29c..9eaefaea 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt @@ -30,7 +30,7 @@ class TextInliner(val text: String) { var index = 0 while (index < text.length) { val char = text.get(index) - + Log.d("TextInliner", "char: " + char + " currentInline: " + currentInline.config.identifier()) if (currentInline.config.type() == MarkdownInlineType.INLINE_CODE && !currentInline.config.isEnd(text, index)) { @@ -143,7 +143,7 @@ class TextInliner(val text: String) { processedSegments.forEach { val inlineConfig = it.config if (inlineConfig is PhraseDelimiterInline && !it.paired) { - it.children.add(NormalInlineMarkdownSegment(inlineConfig.startDelimiter)) + it.children.add(0, NormalInlineMarkdownSegment(inlineConfig.startDelimiter)) } if (!it.paired || it.config.type() == MarkdownInlineType.INVALID) { From c68cb283ec489620c0c69eeb1f5d7d80f3146d38 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Apr 2019 00:03:34 +0100 Subject: [PATCH 003/134] Adding Google Drive Sync --- .../scarlet/base/config/MaterialNoteConfig.kt | 1 - .../sheet/ExternalFolderSyncBottomSheet.kt | 2 + build.gradle | 2 +- scarlet/build.gradle | 20 +- .../java/com/bijoysingh/quicknote/Scarlet.kt | 2 + .../quicknote/drive/GDriveLoginActivity.kt | 71 +++--- .../quicknote/drive/GDriveRemoteDatabase.kt | 206 ++++++++++++++++++ .../quicknote/drive/GDriveRemoveFolder.kt | 149 +++++++++++++ .../quicknote/drive/GDriveServiceHelper.kt | 184 ++++++++++++++++ .../firebase/support/ScarletAuthenticator.kt | 38 +++- .../quicknote/scarlet/ScarletFolderActor.kt | 3 + .../quicknote/scarlet/ScarletNoteActor.kt | 3 + .../quicknote/scarlet/ScarletTagActor.kt | 3 + 13 files changed, 645 insertions(+), 39 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 559fb162..a2c220bf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -22,7 +22,6 @@ import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase -import com.maubis.scarlet.base.export.support.ExternalFolderSync import com.maubis.scarlet.base.support.ui.IThemeManager import com.maubis.scarlet.base.support.ui.ThemeManager import com.maubis.scarlet.base.support.utils.Flavor diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index 0bb3651e..747e429f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -11,6 +11,7 @@ import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.export.support.ExternalFolderSync import com.maubis.scarlet.base.export.support.sExternalFolderSync import com.maubis.scarlet.base.export.support.sFolderSyncBackupLocked import com.maubis.scarlet.base.export.support.sFolderSyncPath @@ -73,6 +74,7 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .isActionNegative(sExternalFolderSync) .onPrimaryClick { sExternalFolderSync = !sExternalFolderSync + ExternalFolderSync.enable(componentContext.androidContext, sExternalFolderSync) reset(componentContext.androidContext, dialog) } .paddingDip(YogaEdge.HORIZONTAL, 20f) diff --git a/build.gradle b/build.gradle index e929ba43..a684175b 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { // NOTE: Keep this in sync with the build.gradle for app/ ext.appconfig_version_code = 125 ext.appconfig_version = '6.9.7' - ext.appconfig_min_os_version = 17 + ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' ext.appconfig_compile_sdk_version = 28 diff --git a/scarlet/build.gradle b/scarlet/build.gradle index dd8db4d3..024e01f7 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -39,6 +39,16 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/license.txt' + exclude 'META-INF/NOTICE' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/notice.txt' + exclude 'META-INF/ASL2.0' + } } dependencies { @@ -50,8 +60,14 @@ dependencies { implementation "com.google.firebase:firebase-auth:16.1.0" implementation "com.google.firebase:firebase-database:16.0.6" - implementation "com.google.android.gms:play-services-auth:16.0.1" - implementation 'com.google.android.gms:play-services-drive:16.0.0' + implementation 'com.google.android.gms:play-services-auth:16.0.1' + implementation 'com.google.http-client:google-http-client-gson:1.26.0' + implementation('com.google.api-client:google-api-client-android:1.26.0') { + exclude group: 'org.apache.httpcomponents' + } + implementation('com.google.apis:google-api-services-drive:v3-rev136-1.25.0') { + exclude group: 'org.apache.httpcomponents' + } } apply plugin: 'kotlin-android-extensions' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index 57b3674d..951abc1c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -1,5 +1,6 @@ package com.bijoysingh.quicknote +import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase import com.bijoysingh.quicknote.firebase.FirebaseRemoteDatabase import com.bijoysingh.quicknote.scarlet.ScarletConfig import com.maubis.scarlet.base.config.ApplicationBase @@ -19,5 +20,6 @@ class Scarlet : ApplicationBase() { companion object { var firebase: FirebaseRemoteDatabase? = null + var gDrive: GDriveRemoteDatabase? = null } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 8fe56d9a..217eeb91 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -4,22 +4,30 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.GoogleApiClient -import com.google.android.gms.drive.Drive -import com.google.android.gms.drive.DriveClient -import com.google.android.gms.drive.DriveResourceClient +import com.google.android.gms.common.api.Scope +import com.google.api.client.extensions.android.http.AndroidHttp +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.json.gson.GsonFactory +import com.google.api.services.drive.Drive +import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.android.synthetic.main.gdrive_login.* +import java.lang.ref.WeakReference +import java.util.* import java.util.concurrent.atomic.AtomicBoolean + // TODO: This is not ready... Recent changes in Drive API make this sh*t a little difficult and // inconclusive. I want to do this because it's safer than Firebase, but f*ck Google for // changing the API So much @@ -31,9 +39,8 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed lateinit var context: Context lateinit var mGoogleApiClient: GoogleApiClient - var mDriveClient: DriveClient? = null - var mDriveResourceClient: DriveResourceClient? = null var loggingIn = AtomicBoolean(false) + var mDriveServiceHelper: GDriveServiceHelper? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,9 +63,10 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder( GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) .requestIdToken(getString(R.string.default_web_client_id)) - .requestScopes(Drive.SCOPE_APPFOLDER) - .requestEmail().build() + .requestEmail() + .build() mGoogleApiClient = GoogleApiClient .Builder(this) @@ -80,9 +88,16 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == RC_SIGN_IN || requestCode == RC_SIGN_IN_PERMISSIONS) { - if (mDriveResourceClient !== null) { - onLoginComplete(context, mDriveResourceClient!!) + if (requestCode == RC_SIGN_IN) { + val task = GoogleSignIn.getSignedInAccountFromIntent(data) + try { + val account = task.getResult(ApiException::class.java) + if (account !== null) { + onLoginComplete(account) + return + } + } catch (exception: Exception) { + // Ignore this, handled by following content } } } @@ -101,14 +116,6 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed } } - private fun recheckPermissions(account: GoogleSignInAccount): Boolean { - if (!GoogleSignIn.hasPermissions(account, Drive.SCOPE_APPFOLDER)) { - GoogleSignIn.requestPermissions(this, RC_SIGN_IN_PERMISSIONS, account, Drive.SCOPE_APPFOLDER) - return false - } - return true - } - override fun notifyThemeChange() { setSystemTheme() containerLayout.setBackgroundColor(getThemeColor()) @@ -116,19 +123,33 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed signInToGDriveDetails.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) } - fun onLoginComplete( - context: Context, - deviceResourceClient: DriveResourceClient) { - val appFolderTask = deviceResourceClient.getAppFolder() - appFolderTask.addOnSuccessListener { folder -> + fun onLoginComplete(account: GoogleSignInAccount) { + mDriveServiceHelper = getDriveHelper(context, account) - }.addOnFailureListener { + gDrive?.reset() + gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) + gDrive?.init(mDriveServiceHelper!!) - } + setButton(false) } override fun onConnectionFailed(p0: ConnectionResult) { ToastHelper.show(this, R.string.google_drive_page_connection_failed) } + companion object { + fun getDriveHelper(context: Context, account: GoogleSignInAccount): GDriveServiceHelper { + val credential = GoogleAccountCredential.usingOAuth2( + context, + Collections.singleton(DriveScopes.DRIVE_FILE)) + credential.selectedAccount = account.account + val googleDriveService = Drive.Builder( + AndroidHttp.newCompatibleTransport(), + GsonFactory(), + credential) + .setApplicationName(context.getString(R.string.app_name)) + .build() + return GDriveServiceHelper(googleDriveService) + } + } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index a5da0890..cea18ede 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,2 +1,208 @@ package com.bijoysingh.quicknote.drive +import android.content.Context +import com.bijoysingh.quicknote.firebase.data.* +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.core.folder.IFolderContainer +import com.maubis.scarlet.base.core.note.INoteContainer +import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.core.tag.ITagContainer +import com.maubis.scarlet.base.database.remote.IRemoteDatabase +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference + +class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDatabase { + + private var isValidController: Boolean = true + private var driveHelper: GDriveServiceHelper? = null + + private var notesSync: GDriveRemoteFolder? = null + private var foldersSync: GDriveRemoteFolder? = null + private var tagsSync: GDriveRemoteFolder? = null + + override fun init(userId: String) {} + + fun init(helper: GDriveServiceHelper) { + isValidController = true + driveHelper = helper + + notesSync = GDriveRemoteFolder( + FirebaseNote::class.java, + helper, + { it -> CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() }, + { it -> onRemoteInsert(it) }, + { it -> onRemoteRemove(FirebaseNote(it, "", 0L, 0L, 0, NoteState.DEFAULT.name, "", false, false, "")) }) + tagsSync = GDriveRemoteFolder( + FirebaseTag::class.java, + helper, + { it -> CoreConfig.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() }, + { it -> onRemoteInsert(it) }, + { it -> onRemoteRemove(FirebaseTag(it, "")) }) + foldersSync = GDriveRemoteFolder( + FirebaseFolder::class.java, + helper, + { it -> CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() }, + { it -> onRemoteInsert(it) }, + { it -> onRemoteRemove(FirebaseFolder(it, "", 0L, 0L, 0)) }) + + GlobalScope.launch { + driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { + when { + (it === null) -> reset() + else -> onRootFolderLoaded(it) + } + } + } + } + + fun onRootFolderLoaded(rootFolderId: String) { + driveHelper?.getOrCreateDirectory(rootFolderId, "notes") { + if (it !== null) { + notesSync?.init(it) + } + } + driveHelper?.getOrCreateDirectory(rootFolderId, "tags") { + if (it !== null) { + tagsSync?.init(it) + } + } + driveHelper?.getOrCreateDirectory(rootFolderId, "folders") { + if (it !== null) { + foldersSync?.init(it) + } + } + driveHelper?.getOrCreateDirectory(rootFolderId, "images") {} + } + + override fun reset() { + isValidController = false + driveHelper = null + } + + override fun logout() { + reset() + } + + override fun deleteEverything() { + if (!isValidController) { + return + } + } + + override fun insert(note: INoteContainer) { + if (!isValidController || note !is FirebaseNote) { + return + } + notesSync?.insert(note.uuid, note) + } + + override fun insert(tag: ITagContainer) { + if (!isValidController || tag !is FirebaseTag) { + return + } + tagsSync?.insert(tag.uuid, tag) + } + + override fun insert(folder: IFolderContainer) { + if (!isValidController || folder !is FirebaseFolder) { + return + } + foldersSync?.insert(folder.uuid, folder) + } + + override fun remove(note: INoteContainer) { + if (!isValidController || note !is FirebaseNote) { + return + } + notesSync?.delete(note.uuid) + } + + override fun remove(tag: ITagContainer) { + if (!isValidController || tag !is FirebaseTag) { + return + } + tagsSync?.delete(tag.uuid) + } + + override fun remove(folder: IFolderContainer) { + if (!isValidController || folder !is FirebaseFolder) { + return + } + foldersSync?.delete(folder.uuid) + } + + override fun onRemoteInsert(note: INoteContainer) { + if (!isValidController || note !is FirebaseNote) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteInsert(context, note) + } + + override fun onRemoteRemove(note: INoteContainer) { + if (!isValidController || note !is FirebaseNote) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteRemove(context, note) + } + + override fun onRemoteInsert(tag: ITagContainer) { + if (!isValidController || tag !is FirebaseTag) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteInsert(context, tag) + } + + override fun onRemoteRemove(tag: ITagContainer) { + if (!isValidController || tag !is FirebaseTag) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteRemove(context, tag) + } + + override fun onRemoteInsert(folder: IFolderContainer) { + if (!isValidController || folder !is FirebaseFolder) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteInsert(context, folder) + } + + override fun onRemoteRemove(folder: IFolderContainer) { + if (!isValidController || folder !is FirebaseFolder) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteRemove(context, folder) + } + +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt new file mode 100644 index 00000000..7378fb0c --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt @@ -0,0 +1,149 @@ +package com.bijoysingh.quicknote.drive + +import com.bijoysingh.quicknote.firebase.data.FirebaseFolder +import com.bijoysingh.quicknote.firebase.data.FirebaseNote +import com.google.gson.Gson +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.export.remote.LAST_MODIFIED_ERROR_MARGIN +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean + +const val KEY_G_DRIVE_SYNC_LAST_SCAN = "drive_g_folder_sync_last_sync" + +class GDriveRemoteFolder( + val klass: Class, + val helper: GDriveServiceHelper, + val uuidToObject: (String) -> T?, + val onRemoteInsert: (T) -> Unit, + val onRemoteDelete: (String) -> Unit) { + + // Mapping from uuid => fileIds + var deletedFileId: String? = null + val loaded = AtomicBoolean(false) + val fileIds = emptyMap().toMutableMap() + val pendingActions = emptyMap().toMutableMap() + val deletedUuids = HashSet() + var folderUid: String = INVALID_FILE_ID + + fun init(fUid: String) { + folderUid = fUid + val lastScanKey = "${KEY_G_DRIVE_SYNC_LAST_SCAN}_$folderUid" + val lastScan = CoreConfig.instance.store().get(lastScanKey, 0L) + + GlobalScope.launch(Dispatchers.IO) { + helper.getFilesInFolder(folderUid).addOnCompleteListener { + val files = it.result?.files + if (files !== null) { + files.forEach { file -> + if (file.mimeType == GOOGLE_DRIVE_FILE_MIME_TYPE + && (file.modifiedTime?.value ?: System.currentTimeMillis() > lastScan - LAST_MODIFIED_ERROR_MARGIN)) { + fileIds[file.name] = file.id + helper.readFile(file.id).addOnCompleteListener { + val data = it.result + if (data !== null) { + try { + val item = Gson().fromJson(data, klass) + if (item !== null) { + onRemoteInsert(item) + } + } catch (exception: Exception) { + } + } + } + } + } + loaded.set(true) + removePendingActions() + scanDeletedFiles() + } + } + } + } + + fun scanDeletedFiles() { + helper.getOrCreateDirectory(folderUid, "delete") { + deletedFileId = it + if (deletedFileId !== null) { + helper.getFilesInFolder(deletedFileId!!).addOnCompleteListener { + val files = it.result?.files + if (files !== null) { + files.forEach { file -> + if (file.mimeType == GOOGLE_DRIVE_FILE_MIME_TYPE) { + deletedUuids.add(file.name) + onRemoteDelete(file.name) + } + } + } + } + removePendingActions() + } + } + } + + fun removePendingActions() { + val tPendingActions = emptyMap().toMutableMap() + tPendingActions.putAll(pendingActions) + pendingActions.clear() + + GlobalScope.launch { + tPendingActions.forEach { + when (it.value) { + "insert" -> { + val item = uuidToObject(it.key) + if (item !== null) { + insert(it.key, item) + } + } + "delete" -> delete(it.key) + } + } + } + } + + fun insert(uuid: String, item: T) { + if (!loaded.get()) { + pendingActions[uuid] = "insert" + return + } + + try { + val data = Gson().toJson(item) + val fileId = fileIds.get(uuid) + if (fileId !== null) { + helper.saveFile(fileId, uuid, data) + return + } + + val modificationTime = when { + item is FirebaseNote -> item.updateTimestamp + item is FirebaseFolder -> item.updateTimestamp + else -> null + } + helper.createFile(folderUid, modificationTime).addOnCompleteListener { + val createdFileId = it.result + if (createdFileId !== null) { + fileIds[uuid] = createdFileId + helper.saveFile(createdFileId, uuid, data) + } + } + } catch (exception: Exception) { + } + } + + fun delete(uuid: String) { + if (!loaded.get() || deletedFileId === null) { + pendingActions[uuid] = "delete" + return + } + + fileIds.remove(uuid) + helper.createFile(deletedFileId!!).addOnCompleteListener { + val removeFileId = it.result + if (removeFileId !== null) { + helper.saveFile(removeFileId, uuid, System.currentTimeMillis().toString()) + } + } + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt new file mode 100644 index 00000000..80a70561 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -0,0 +1,184 @@ +package com.bijoysingh.quicknote.drive + +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +import android.provider.OpenableColumns +import android.support.v4.util.Pair +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.api.client.http.ByteArrayContent +import com.google.api.client.util.DateTime +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import com.google.api.services.drive.model.FileList +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.util.concurrent.Callable +import java.util.concurrent.Executors + +const val GOOGLE_DRIVE_ROOT_FOLDER = "Scarlet (App Data)" + +const val GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder" +const val GOOGLE_DRIVE_FILE_MIME_TYPE = "text/plain" + +const val INVALID_FILE_ID = "__invalid__" + +class GDriveServiceHelper(private val mDriveService: Drive) { + private val mExecutor = Executors.newSingleThreadExecutor() + + /** + * Creates a text file in the user's My Drive folder and returns its file ID. + */ + fun createFile(folderId: String, modificationTimeOverride: Long? = null): Task { + return Tasks.call(mExecutor, Callable { + val metadata = File() + .setParents(listOf(folderId)) + .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) + .setModifiedTime(DateTime(modificationTimeOverride ?: System.currentTimeMillis())) + .setName("file") + + val googleFile = mDriveService.files().create(metadata).execute() + googleFile?.id ?: INVALID_FILE_ID + }) + } + + fun createFolder(parentUid: String, folderName: String): Task { + return Tasks.call(mExecutor, Callable { + val metadata = File() + .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) + .setName(folderName) + if (!parentUid.isEmpty()) { + metadata.parents = listOf(parentUid) + } + val googleFile = mDriveService.files().create(metadata).execute() + googleFile?.id ?: INVALID_FILE_ID + }) + } + + + fun readFile(fileId: String): Task { + return Tasks.call(mExecutor, Callable { + // Retrieve the metadata as a File object. + val metadata = mDriveService.files().get(fileId).execute() + val name = metadata.name + + // Stream the file contents to a String. + mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> + BufferedReader(InputStreamReader(`is`)).use { reader -> + val stringBuilder = StringBuilder() + var line: String? = reader.readLine() + while (line !== null) { + stringBuilder.append(line) + line = reader.readLine() + } + val contents = stringBuilder.toString() + contents + } + } + }) + } + + /** + * Updates the file identified by `fileId` with the given `name` and `content`. + */ + fun saveFile(fileId: String, name: String, content: String): Task { + return Tasks.call(mExecutor, Callable { + // Create a File containing any metadata changes. + val metadata = File().setName(name) + + // Convert content to an AbstractInputStreamContent instance. + val contentStream = ByteArrayContent.fromString("text/plain", content) + + // Update the metadata and contents. + mDriveService.files().update(fileId, metadata, contentStream).execute() + + null + }) + } + + fun getFilesInFolder(parentUid: String): Task { + return Tasks.call(mExecutor, Callable { + mDriveService.files().list() + .setSpaces("drive") + .setPageSize(1000) + .setQ("mimeType = '$GOOGLE_DRIVE_FILE_MIME_TYPE' and '$parentUid' in parents") + .execute() + }) + } + + fun getFolderQuery(parentUid: String, name: String): Task { + val query = when { + parentUid.isEmpty() -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name'" + else -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name' and '$parentUid' in parents" + } + return Tasks.call(mExecutor, Callable { + mDriveService.files().list() + .setSpaces("drive") + .setQ(query) + .execute() + }) + } + + fun getOrCreateDirectory(parentUid: String, name: String, onFolderId: (String?) -> Unit) { + getFolderQuery(parentUid, name).addOnCompleteListener { getTask -> + val fid = getTask.result?.files?.firstOrNull()?.id + if (fid !== null) { + onFolderId(fid) + return@addOnCompleteListener + } + + createFolder(parentUid, name).addOnCompleteListener { createTask -> + onFolderId(createTask.result) + } + } + } + + /** + * Returns an [Intent] for opening the Storage Access Framework file picker. + */ + fun createFilePickerIntent(): Intent { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + + return intent + } + + /** + * Opens the file at the `uri` returned by a Storage Access Framework [Intent] + * created by [.createFilePickerIntent] using the given `contentResolver`. + */ + fun openFileUsingStorageAccessFramework( + contentResolver: ContentResolver, uri: Uri): Task> { + return Tasks.call(mExecutor, Callable { + // Retrieve the document's display name from its metadata. + var name: String = "" + contentResolver.query(uri, null, null, null, null)!!.use { cursor -> + if (cursor != null && cursor.moveToFirst()) { + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + name = cursor.getString(nameIndex) + } else { + throw IOException("Empty cursor returned for file.") + } + } + + // Read the document's contents as a String. + var content: String = "" + contentResolver.openInputStream(uri)!!.use { `is` -> + BufferedReader(InputStreamReader(`is`)).use { reader -> + val stringBuilder = StringBuilder() + var line: String = reader.readLine() + while (line != null) { + stringBuilder.append(line) + line = reader.readLine() + } + content = stringBuilder.toString() + } + } + + Pair.create(name, content) + }) + } +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index 89eba488..b9224fab 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -5,11 +5,15 @@ import android.content.Intent import android.os.Handler import android.os.Looper import com.bijoysingh.quicknote.Scarlet.Companion.firebase +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.drive.GDriveLoginActivity +import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.activity.LoginActivity import com.bijoysingh.quicknote.firebase.initFirebaseDatabase import com.github.bijoysingh.starter.async.SimpleThreadExecutor import com.github.bijoysingh.starter.util.ToastHelper +import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseNetworkException import com.google.firebase.auth.FirebaseAuth @@ -17,6 +21,9 @@ import com.google.firebase.database.FirebaseDatabase import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference class ScarletAuthenticator() : IAuthenticator { override fun userId(): String? { @@ -25,18 +32,29 @@ class ScarletAuthenticator() : IAuthenticator { } override fun setup(context: Context) { - FirebaseApp.initializeApp(context) - try { - val userId = userId() - if (userId === null) { - return + GlobalScope.launch { + val account = GoogleSignIn.getLastSignedInAccount(context) + if (account !== null) { + val helper= GDriveLoginActivity.getDriveHelper(context, account) + gDrive = GDriveRemoteDatabase(WeakReference(context)) + gDrive?.init(helper) } + } - FirebaseDatabase.getInstance() - initFirebaseDatabase(context, userId) - reloadUser(context) - } catch (exception: Exception) { - // Don't need to do anything + GlobalScope.launch { + FirebaseApp.initializeApp(context) + try { + val userId = userId() + if (userId === null) { + return@launch + } + + FirebaseDatabase.getInstance() + initFirebaseDatabase(context, userId) + reloadUser(context) + } catch (exception: Exception) { + // Don't need to do anything + } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 9bf6b528..89731a2a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.firebase +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseFolder import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.core.folder.MaterialFolderActor @@ -10,10 +11,12 @@ class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { override fun onlineSave() { super.onlineSave() firebase?.insert(folder.getFirebaseFolder()) + gDrive?.insert(folder.getFirebaseFolder()) } override fun delete() { super.delete() firebase?.remove(folder.getFirebaseFolder()) + gDrive?.remove(folder.getFirebaseFolder()) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index d645825e..abc334e0 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context import com.bijoysingh.quicknote.Scarlet.Companion.firebase +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.note.MaterialNoteActor @@ -11,10 +12,12 @@ class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { override fun onlineSave(context: Context) { super.onlineSave(context) firebase?.insert(note.getFirebaseNote()) + gDrive?.insert(note.getFirebaseNote()) } override fun onlineDelete(context: Context) { super.onlineDelete(context) firebase?.remove(note.getFirebaseNote()) + gDrive?.remove(note.getFirebaseNote()) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 8a500b70..e890beb9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.firebase +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseTag import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.core.tag.MaterialTagActor @@ -10,10 +11,12 @@ class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { override fun onlineSave() { super.onlineSave() firebase?.insert(tag.getFirebaseTag()) + gDrive?.insert(tag.getFirebaseTag()) } override fun delete() { super.delete() firebase?.remove(tag.getFirebaseTag()) + gDrive?.insert(tag.getFirebaseTag()) } } \ No newline at end of file From f71f9343752035567269cf004bc49c003836216d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Apr 2019 00:14:51 +0100 Subject: [PATCH 004/134] Chaning how external folder sync is accessed --- .../scarlet/base/config/ApplicationBase.kt | 5 +++++ .../maubis/scarlet/base/config/CoreConfig.kt | 7 ++----- .../scarlet/base/config/MaterialNoteConfig.kt | 5 ----- .../base/core/folder/MaterialFolderActor.kt | 5 +++-- .../base/core/note/MaterialNoteActor.kt | 10 +++++----- .../scarlet/base/core/tag/MaterialTagActor.kt | 5 +++-- .../base/export/support/ExternalFolderSync.kt | 20 +++++++++++-------- .../base/main/sheets/WhatsNewBottomSheet.kt | 2 +- 8 files changed, 31 insertions(+), 28 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index 349bf02a..bcdee15b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -3,6 +3,7 @@ package com.maubis.scarlet.base.config import android.app.Application import com.evernote.android.job.JobManager import com.facebook.soloader.SoLoader +import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator abstract class ApplicationBase : Application() { @@ -11,4 +12,8 @@ abstract class ApplicationBase : Application() { SoLoader.init(this, false) JobManager.create(this).addJobCreator(ReminderJobCreator()) } + + companion object { + var folderSync: FolderRemoteDatabase? = null + } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 6c6576f4..a0f7cdb8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -6,7 +6,6 @@ import android.support.v4.content.res.ResourcesCompat import android.support.v7.app.AppCompatActivity import com.github.ajalt.reprint.core.Reprint import com.github.bijoysingh.starter.prefs.Store -import com.maubis.markdown.MarkdownConfig import com.maubis.markdown.MarkdownConfig.Companion.config import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.auth.IAuthenticator @@ -21,7 +20,6 @@ import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.support.ui.IThemeManager import com.maubis.scarlet.base.support.utils.Flavor import com.maubis.scarlet.base.support.utils.ImageCache @@ -30,7 +28,8 @@ abstract class CoreConfig(context: Context) { init { Reprint.initialize(context) - config.spanConfig.headingTypeface = ResourcesCompat.getFont(context, R.font.monserrat) ?: Typeface.DEFAULT + config.spanConfig.headingTypeface = ResourcesCompat.getFont(context, R.font.monserrat) + ?: Typeface.DEFAULT FONT_MONSERRAT = config.spanConfig.headingTypeface FONT_OPEN_SANS = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT } @@ -61,8 +60,6 @@ abstract class CoreConfig(context: Context) { abstract fun store(): Store - abstract fun externalFolderSync(): FolderRemoteDatabase - abstract fun imageCache(): ImageCache companion object { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index a2c220bf..4ea9b0ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -21,12 +21,10 @@ import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.support.ui.IThemeManager import com.maubis.scarlet.base.support.ui.ThemeManager import com.maubis.scarlet.base.support.utils.Flavor import com.maubis.scarlet.base.support.utils.ImageCache -import java.lang.ref.WeakReference const val USER_PREFERENCES_STORE_NAME = "USER_PREFERENCES"; const val USER_PREFERENCES_VERSION = 1; @@ -39,7 +37,6 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { val foldersProvider = FoldersProvider() val store = VersionedStore.get(context, USER_PREFERENCES_STORE_NAME, USER_PREFERENCES_VERSION) val appTheme = ThemeManager() - val externalFolderSync = FolderRemoteDatabase(WeakReference(context)) val imageCache = ImageCache(context) override fun database(): AppDatabase = db @@ -68,7 +65,5 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun store(): Store = store - override fun externalFolderSync(): FolderRemoteDatabase = externalFolderSync - override fun imageCache(): ImageCache = imageCache } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt index d52aff45..972a5a55 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.core.folder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.export.data.ExportableFolder @@ -13,7 +14,7 @@ open class MaterialFolderActor(val folder: Folder) : IFolderActor { } override fun onlineSave() { - CoreConfig.instance.externalFolderSync().insert(ExportableFolder(folder)) + folderSync?.insert(ExportableFolder(folder)) } override fun save() { @@ -32,7 +33,7 @@ open class MaterialFolderActor(val folder: Folder) : IFolderActor { override fun delete() { offlineDelete() - CoreConfig.instance.externalFolderSync().remove(ExportableFolder(folder)) + folderSync?.remove(ExportableFolder(folder)) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index 24a35df4..b7c25dbd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -8,18 +8,18 @@ import android.support.v7.app.AppCompatActivity import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.main.activity.WidgetConfigureActivity import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler -import com.maubis.scarlet.base.widget.AllNotesWidgetProvider.Companion.notifyAllChanged import com.maubis.scarlet.base.service.FloatingNoteService -import com.maubis.scarlet.base.support.utils.ImageCache +import com.maubis.scarlet.base.widget.AllNotesWidgetProvider.Companion.notifyAllChanged import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.util.* @@ -51,7 +51,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { } override fun onlineSave(context: Context) { - CoreConfig.instance.externalFolderSync().insert(ExportableNote(note)) + folderSync?.insert(ExportableNote(note)) } override fun save(context: Context) { @@ -94,7 +94,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { override fun onlineDelete(context: Context) { - CoreConfig.instance.externalFolderSync().remove(ExportableNote(note)) + folderSync?.remove(ExportableNote(note)) } override fun delete(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt index c078e8e8..90c60cfd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.core.tag +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.data.ExportableTag @@ -12,7 +13,7 @@ open class MaterialTagActor(val tag: Tag) : ITagActor { } override fun onlineSave() { - CoreConfig.instance.externalFolderSync().insert(ExportableTag(tag)) + ApplicationBase.folderSync?.insert(ExportableTag(tag)) } override fun save() { @@ -31,7 +32,7 @@ open class MaterialTagActor(val tag: Tag) : ITagActor { override fun delete() { offlineDelete() - CoreConfig.instance.externalFolderSync().remove(ExportableTag(tag)) + ApplicationBase.folderSync?.remove(ExportableTag(tag)) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index d7acb3e5..59a564b1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -7,14 +7,17 @@ import android.os.Build import android.support.v4.content.ContextCompat import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.export.data.ExportableFolder import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.export.data.ExportableTag +import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FOLDER import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.lang.ref.WeakReference const val KEY_EXTERNAL_FOLDER_SYNC_ENABLED = "external_folder_sync_enabled" @@ -45,7 +48,7 @@ object ExternalFolderSync { if (!hasPermission(context)) { GlobalScope.launch(Dispatchers.Main) { ToastHelper.show(context, R.string.permission_layout_give_permission_details) - CoreConfig.instance.externalFolderSync().reset() + folderSync?.reset() } return } @@ -53,25 +56,25 @@ object ExternalFolderSync { loadFirstTime() } else { sExternalFolderSync = false - CoreConfig.instance.externalFolderSync().reset() + folderSync?.reset() } } fun loadFirstTime() { - CoreConfig.instance.externalFolderSync().init( + folderSync?.init( { CoreConfig.instance.notesDatabase().getAll().forEach { - CoreConfig.instance.externalFolderSync().insert(ExportableNote(it)) + folderSync?.insert(ExportableNote(it)) } }, { CoreConfig.instance.tagsDatabase().getAll().forEach { - CoreConfig.instance.externalFolderSync().insert(ExportableTag(it)) + folderSync?.insert(ExportableTag(it)) } }, { CoreConfig.instance.foldersDatabase().getAll().forEach { - CoreConfig.instance.externalFolderSync().insert(ExportableFolder(it)) + folderSync?.insert(ExportableFolder(it)) } }) } @@ -81,10 +84,11 @@ object ExternalFolderSync { return } - if (!hasPermission(context)) { + if (!hasPermission(context)) { sExternalFolderSync = false return } - CoreConfig.instance.externalFolderSync().init() + folderSync = FolderRemoteDatabase(WeakReference(context)) + folderSync?.init() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 0d3c5c82..5694d30a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -82,7 +82,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { "- **Realtime Markdown:** When you type in markdown you get real time conversion and formatting.\n\n" + "- **More Editor Options:** Head over to settings to get more control on the editor experience.\n\n" + "- **More Themes:** Pro Users get more themes for the app, and the default dark theme is even darker now.\n\n" + - "- **Folder Sync:** Sync all your notes to an external folder live along with images.\n\n" + + "- **Folder Sync:** Sync all your notes to an folderSync folder live along with images.\n\n" + "- **Widget Options:** Widgets now show formatted text! You can also configure what notes to see in the widget.\n\n" + "Even more little things which help you enjoy using this app everyday" val WHATS_NEW_DETAILS_LAST_RELEASE_MD = From cfbca9bfd02dd84e79a77ec828fd1163836e4b78 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Apr 2019 00:49:02 +0100 Subject: [PATCH 005/134] [Google Drive Sync] Fixing initial data sync --- .../base/export/support/ExternalFolderSync.kt | 2 +- .../quicknote/drive/GDriveLoginActivity.kt | 20 +++++++++++++ .../quicknote/drive/GDriveRemoteDatabase.kt | 28 +++++++++++++++++-- .../quicknote/drive/GDriveRemoveFolder.kt | 3 +- .../firebase/support/ScarletAuthenticator.kt | 1 + 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index 59a564b1..5f24c9b4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -84,7 +84,7 @@ object ExternalFolderSync { return } - if (!hasPermission(context)) { + if (!hasPermission(context)) { sExternalFolderSync = false return } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 217eeb91..4bc1b7d4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -20,6 +20,7 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.export.support.KEY_EXTERNAL_FOLDER_SYNC_ENABLED import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.android.synthetic.main.gdrive_login.* @@ -31,6 +32,21 @@ import java.util.concurrent.atomic.AtomicBoolean // TODO: This is not ready... Recent changes in Drive API make this sh*t a little difficult and // inconclusive. I want to do this because it's safer than Firebase, but f*ck Google for // changing the API So much + +const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" + +var sGDriveFirstSyncNote: Boolean + get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) + set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) +var sGDriveFirstSyncTag: Boolean + get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) + set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) +var sGDriveFirstSyncFolder: Boolean + get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) + set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) + class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { private val RC_SIGN_IN = 31244 @@ -126,6 +142,10 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed fun onLoginComplete(account: GoogleSignInAccount) { mDriveServiceHelper = getDriveHelper(context, account) + sGDriveFirstSyncNote = false + sGDriveFirstSyncFolder = false + sGDriveFirstSyncTag = false + gDrive?.reset() gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) gDrive?.init(mDriveServiceHelper!!) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index cea18ede..5691cb65 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote.drive import android.content.Context +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.* import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.IFolderContainer @@ -60,17 +61,38 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat fun onRootFolderLoaded(rootFolderId: String) { driveHelper?.getOrCreateDirectory(rootFolderId, "notes") { if (it !== null) { - notesSync?.init(it) + notesSync?.init(it) { + if (!sGDriveFirstSyncNote) { + CoreConfig.instance.notesDatabase().getAll().forEach { + gDrive?.insert(it.getFirebaseNote()) + } + sGDriveFirstSyncNote = true + } + } } } driveHelper?.getOrCreateDirectory(rootFolderId, "tags") { if (it !== null) { - tagsSync?.init(it) + tagsSync?.init(it) { + if (!sGDriveFirstSyncTag) { + CoreConfig.instance.tagsDatabase().getAll().forEach { + gDrive?.insert(it.getFirebaseTag()) + } + sGDriveFirstSyncTag = true + } + } } } driveHelper?.getOrCreateDirectory(rootFolderId, "folders") { if (it !== null) { - foldersSync?.init(it) + foldersSync?.init(it) { + if (!sGDriveFirstSyncFolder) { + CoreConfig.instance.foldersDatabase().getAll().forEach { + gDrive?.insert(it.getFirebaseFolder()) + } + sGDriveFirstSyncFolder = true + } + } } } driveHelper?.getOrCreateDirectory(rootFolderId, "images") {} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt index 7378fb0c..5bf2d0d1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt @@ -27,7 +27,7 @@ class GDriveRemoteFolder( val deletedUuids = HashSet() var folderUid: String = INVALID_FILE_ID - fun init(fUid: String) { + fun init(fUid: String, onLoaded: () -> Unit) { folderUid = fUid val lastScanKey = "${KEY_G_DRIVE_SYNC_LAST_SCAN}_$folderUid" val lastScan = CoreConfig.instance.store().get(lastScanKey, 0L) @@ -57,6 +57,7 @@ class GDriveRemoteFolder( loaded.set(true) removePendingActions() scanDeletedFiles() + GlobalScope.launch { onLoaded() } } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index b9224fab..f34e328d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -68,6 +68,7 @@ class ScarletAuthenticator() : IAuthenticator { } override fun openLoginActivity(context: Context) = Runnable { + // context.startActivity(Intent(context, GDriveLoginActivity::class.java)) context.startActivity(Intent(context, LoginActivity::class.java)) } From a9aa0a57b5311ba03c94d9bb0fc6d869a5e1230c Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Apr 2019 12:54:21 +0100 Subject: [PATCH 006/134] [Google Drive Sync] Adding initial code for image sync --- .../scarlet/base/config/ApplicationBase.kt | 4 + .../base/core/note/MaterialNoteActor.kt | 3 +- .../scarlet/base/database/FoldersProvider.kt | 5 + .../scarlet/base/database/NotesProvider.kt | 5 + .../scarlet/base/database/TagsProvider.kt | 5 + .../scarlet/base/note/NoteExtensions.kt | 11 +- .../creation/activity/CreateNoteActivity.kt | 5 +- .../creation/sheet/FormatActionBottomSheet.kt | 4 +- .../formats/recycler/FormatImageViewHolder.kt | 5 +- .../recycler/NoteRecyclerViewHolderBase.kt | 3 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 32 +++++- .../drive/GDriveRemoteImageFolder.kt | 103 ++++++++++++++++++ .../quicknote/drive/GDriveRemoveFolder.kt | 16 ++- .../quicknote/drive/GDriveServiceHelper.kt | 84 +++----------- 14 files changed, 200 insertions(+), 85 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index bcdee15b..c74dde17 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -3,6 +3,8 @@ package com.maubis.scarlet.base.config import android.app.Application import com.evernote.android.job.JobManager import com.facebook.soloader.SoLoader +import com.maubis.scarlet.base.core.note.NoteImage +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator @@ -11,9 +13,11 @@ abstract class ApplicationBase : Application() { super.onCreate() SoLoader.init(this, false) JobManager.create(this).addJobCreator(ReminderJobCreator()) + noteImagesFolder = NoteImage(this) } companion object { + lateinit var noteImagesFolder: NoteImage var folderSync: FolderRemoteDatabase? = null } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index b7c25dbd..a45ba133 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -9,6 +9,7 @@ import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.format.FormatBuilder @@ -68,7 +69,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { } override fun offlineDelete(context: Context) { - NoteImage(context).deleteAllFiles(note) + noteImagesFolder.deleteAllFiles(note) if (note.isUnsaved()) { return } diff --git a/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt index ca9cf894..5a2d15c3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt @@ -26,6 +26,11 @@ class FoldersProvider { return folders.size } + fun getUUIDs(): List { + maybeLoadFromDB() + return folders.values.map { it.uuid } + } + fun getAll(): List { maybeLoadFromDB() return folders.values.toList() diff --git a/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt index 3857de91..c19296ed 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt @@ -27,6 +27,11 @@ class NotesProvider { return notes.size } + fun getUUIDs(): List { + maybeLoadFromDB() + return notes.values.map { it.uuid } + } + fun getAll(): List { maybeLoadFromDB() return notes.values.toList() diff --git a/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt index d0bd5abf..4a3411dd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt @@ -24,6 +24,11 @@ class TagsProvider { return tags.size } + fun getUUIDs(): List { + maybeLoadFromDB() + return tags.values.map { it.uuid } + } + fun getAll(): List { maybeLoadFromDB() return tags.values.toList() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index f5ca2348..0bab87c8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -2,7 +2,6 @@ package com.maubis.scarlet.base.note import android.content.Context import android.content.Intent -import android.util.Log import com.github.bijoysingh.starter.util.DateFormatter import com.google.gson.Gson import com.maubis.markdown.Markdown @@ -134,17 +133,19 @@ fun Note.getMarkdownText(isMarkdownEnabled: Boolean): CharSequence { } fun Note.getFullText(): String { - val formats = getFormats() - return formats.map { it -> it.markdownText }.joinToString(separator = "\n").trim() + return getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim() } fun Note.getAlphabets(): String { - val formats = getFormats() - return formats.map { it -> it.markdownText }.joinToString(separator = "\n").trim().filter { + return getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim().filter { ((it in 'a'..'z') || (it in 'A'..'Z')) } } +fun Note.getImageIds(): Set { + return getFormats().filter { it.formatType == FormatType.IMAGE }.map { it.text }.toSet() +} + fun Note.getUnreliablyStrippedText(context: Context): String { val builder = StringBuilder() builder.append(Markdown.render(removeMarkdownHeaders(getTitle())), true) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index e0045986..a71b09aa 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -9,6 +9,7 @@ import android.support.v7.widget.helper.ItemTouchHelper import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder @@ -132,7 +133,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { return } - val targetFile = NoteImage(context).renameOrCopy(note!!, imageFile) + val targetFile = noteImagesFolder.renameOrCopy(note!!, imageFile) val index = getFormatIndex(type) triggerImageLoaded(index, targetFile) } @@ -294,7 +295,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { val formatToChange = formats[position] if (!formatToChange.text.isBlank()) { - val noteImage = NoteImage(context) + val noteImage = noteImagesFolder deleteIfExist(noteImage.getFile(note!!.uuid, formatToChange.text)) } formatToChange.text = file.name diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt index a2f88877..2235e3d5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt @@ -5,6 +5,7 @@ import android.view.View import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.NoteImage @@ -80,8 +81,7 @@ class FormatActionBottomSheet : GridBottomSheetBase() { listener = View.OnClickListener { activity.deleteFormat(format) if (format.formatType === FormatType.IMAGE && !format.text.isBlank()) { - val noteImage = NoteImage(themedContext()) - deleteIfExist(noteImage.getFile(noteUUID, format)) + deleteIfExist(noteImagesFolder.getFile(noteUUID, format)) } dismiss() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index d1957b3c..cd6a1fce 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -9,6 +9,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.util.ToastHelper import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.note.ImageLoadCallback import com.maubis.scarlet.base.core.note.NoteImage @@ -77,7 +78,7 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase val fileName = data.text if (!fileName.isBlank()) { - val file = NoteImage(context).getFile(config.noteUUID, data) + val file = noteImagesFolder.getFile(config.noteUUID, data) when (file.exists()) { true -> populateFile(file) false -> { @@ -91,7 +92,7 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase } fun populateFile(file: File) { - NoteImage(context).loadPersistentFileToImageView(image, file, object : ImageLoadCallback { + noteImagesFolder.loadPersistentFileToImageView(image, file, object : ImageLoadCallback { override fun onSuccess() { noImageMessage.visibility = View.GONE } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt index 29cb489c..cf6da641 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt @@ -11,6 +11,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.core.note.NoteState @@ -88,7 +89,7 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi val isImageAvailable = !note.imageSource.isBlank() image.visibility = visibility(isImageAvailable) if (isImageAvailable) { - NoteImage(context).loadThumbnailFileToImageView(note.note.uuid, note.imageSource, image) + noteImagesFolder.loadThumbnailFileToImageView(note.note.uuid, note.imageSource, image) } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 5691cb65..e7f2644f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -10,6 +10,7 @@ import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.tag.ITagContainer import com.maubis.scarlet.base.database.remote.IRemoteDatabase import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils +import com.maubis.scarlet.base.note.getImageIds import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference @@ -22,6 +23,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat private var notesSync: GDriveRemoteFolder? = null private var foldersSync: GDriveRemoteFolder? = null private var tagsSync: GDriveRemoteFolder? = null + private var imageSync: GDriveRemoteImageFolder? = null override fun init(userId: String) {} @@ -47,6 +49,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat { it -> CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() }, { it -> onRemoteInsert(it) }, { it -> onRemoteRemove(FirebaseFolder(it, "", 0L, 0L, 0)) }) + imageSync = GDriveRemoteImageFolder(helper) GlobalScope.launch { driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { @@ -67,6 +70,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat gDrive?.insert(it.getFirebaseNote()) } sGDriveFirstSyncNote = true + } else { + val ids = CoreConfig.instance.notesDatabase().getUUIDs() + notesSync?.notifyingExistingIds(ids) } } } @@ -79,6 +85,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat gDrive?.insert(it.getFirebaseTag()) } sGDriveFirstSyncTag = true + } else { + val ids = CoreConfig.instance.tagsDatabase().getUUIDs() + tagsSync?.notifyingExistingIds(ids) } } } @@ -91,16 +100,37 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat gDrive?.insert(it.getFirebaseFolder()) } sGDriveFirstSyncFolder = true + } else { + val ids = CoreConfig.instance.foldersDatabase().getUUIDs() + foldersSync?.notifyingExistingIds(ids) } } } } - driveHelper?.getOrCreateDirectory(rootFolderId, "images") {} + driveHelper?.getOrCreateDirectory(rootFolderId, "images") { + if (it !== null) { + imageSync?.init(it) { + val imageIds = emptySet().toMutableSet() + CoreConfig.instance.notesDatabase().getAll().map { + Pair(it.uuid, it.getImageIds()) + }.forEach { idImagesPair -> + idImagesPair.second.forEach { imageId -> + imageIds.add(ImageUUID(idImagesPair.first, imageId)) + } + } + imageSync?.notifyingExistingIds(imageIds) + } + } + } } override fun reset() { isValidController = false driveHelper = null + notesSync = null + foldersSync = null + tagsSync = null + imageSync = null } override fun logout() { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt new file mode 100644 index 00000000..957c931c --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -0,0 +1,103 @@ +package com.bijoysingh.quicknote.drive + +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils.onRemoteInsert +import com.maubis.scarlet.base.export.remote.LAST_MODIFIED_ERROR_MARGIN +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +data class ImageUUID( + val noteUuid: String, + val imageUuid: String) { + override fun equals(other: Any?): Boolean { + if (other !is ImageUUID) { + return false + } + return other.imageUuid == imageUuid && other.noteUuid == noteUuid + } + + override fun hashCode(): Int { + return Objects.hash(noteUuid, imageUuid) + } +} + +class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { + + val loaded = AtomicBoolean(false) + val imageFileIds = emptyMap().toMutableMap() + val pendingActions = emptyMap().toMutableMap() + var folderUid: String = INVALID_FILE_ID + + fun init(fUid: String, onLoaded: () -> Unit) { + folderUid = fUid + val lastScanKey = "${KEY_G_DRIVE_SYNC_LAST_SCAN}_$folderUid" + val lastScan = CoreConfig.instance.store().get(lastScanKey, 0L) + + GlobalScope.launch(Dispatchers.IO) { + helper.getFilesInFolder(folderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { + val imageFiles = it.result?.files + if (imageFiles !== null) { + imageFiles.forEach { imageFile -> + val components = imageFile.name.split("::") + if (components.size == 2) { + val noteUuid = components[0] + val imageId = components[1] + imageFileIds[ImageUUID(noteUuid, imageId)] = imageFile.id + } + } + loaded.set(true) + GlobalScope.launch { onLoaded() } + } + } + } + } + + fun notifyingExistingIds(localImages: Set) { + if (!loaded.get()) { + return + } + + val remoteImages = imageFileIds.keys.toHashSet() + localImages.filter { !remoteImages.contains(it) }.forEach { + GlobalScope.launch { + onInsert(it) + } + } + } + + fun onInsert(id: ImageUUID) { + if (!loaded.get()) { + pendingActions.put(id, "insert") + return + } + + if (imageFileIds.containsKey(id)) { + return + } + + val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) + helper.createFile(folderId = folderUid, mimeType = GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { + val createdFileId = it.result + if (createdFileId !== null) { + helper.saveFile(createdFileId, imageFile) + } + } + } + + fun onRemove(id: ImageUUID) { + if (!loaded.get()) { + pendingActions.put(id, "remove") + return + } + + + } + + fun onRemoteInsert(id: ImageUUID) { + + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt index 5bf2d0d1..af68a9e9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt @@ -37,9 +37,9 @@ class GDriveRemoteFolder( val files = it.result?.files if (files !== null) { files.forEach { file -> + fileIds[file.name] = file.id if (file.mimeType == GOOGLE_DRIVE_FILE_MIME_TYPE && (file.modifiedTime?.value ?: System.currentTimeMillis() > lastScan - LAST_MODIFIED_ERROR_MARGIN)) { - fileIds[file.name] = file.id helper.readFile(file.id).addOnCompleteListener { val data = it.result if (data !== null) { @@ -103,6 +103,20 @@ class GDriveRemoteFolder( } } + fun notifyingExistingIds(ids: List) { + if (!loaded.get()) { + return + } + + val uuids = fileIds.keys.toHashSet() + ids.filter { !uuids.contains(it) }.forEach { + val item = uuidToObject(it) + if (item !== null) { + insert(it, item) + } + } + } + fun insert(uuid: String, item: T) { if (!loaded.get()) { pendingActions[uuid] = "insert" diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 80a70561..37ebe3b4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -1,19 +1,14 @@ package com.bijoysingh.quicknote.drive -import android.content.ContentResolver -import android.content.Intent -import android.net.Uri -import android.provider.OpenableColumns -import android.support.v4.util.Pair import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.api.client.http.ByteArrayContent +import com.google.api.client.http.FileContent import com.google.api.client.util.DateTime import com.google.api.services.drive.Drive import com.google.api.services.drive.model.File import com.google.api.services.drive.model.FileList import java.io.BufferedReader -import java.io.IOException import java.io.InputStreamReader import java.util.concurrent.Callable import java.util.concurrent.Executors @@ -22,20 +17,18 @@ const val GOOGLE_DRIVE_ROOT_FOLDER = "Scarlet (App Data)" const val GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder" const val GOOGLE_DRIVE_FILE_MIME_TYPE = "text/plain" +const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" class GDriveServiceHelper(private val mDriveService: Drive) { private val mExecutor = Executors.newSingleThreadExecutor() - /** - * Creates a text file in the user's My Drive folder and returns its file ID. - */ - fun createFile(folderId: String, modificationTimeOverride: Long? = null): Task { + fun createFile(folderId: String, modificationTimeOverride: Long? = null, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { return Tasks.call(mExecutor, Callable { val metadata = File() .setParents(listOf(folderId)) - .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) + .setMimeType(mimeType) .setModifiedTime(DateTime(modificationTimeOverride ?: System.currentTimeMillis())) .setName("file") @@ -60,11 +53,9 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun readFile(fileId: String): Task { return Tasks.call(mExecutor, Callable { - // Retrieve the metadata as a File object. val metadata = mDriveService.files().get(fileId).execute() val name = metadata.name - // Stream the file contents to a String. mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> BufferedReader(InputStreamReader(`is`)).use { reader -> val stringBuilder = StringBuilder() @@ -80,30 +71,30 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - /** - * Updates the file identified by `fileId` with the given `name` and `content`. - */ fun saveFile(fileId: String, name: String, content: String): Task { return Tasks.call(mExecutor, Callable { - // Create a File containing any metadata changes. val metadata = File().setName(name) - - // Convert content to an AbstractInputStreamContent instance. val contentStream = ByteArrayContent.fromString("text/plain", content) - - // Update the metadata and contents. mDriveService.files().update(fileId, metadata, contentStream).execute() + null + }) + } + fun saveFile(fileId: String, file: java.io.File): Task { + return Tasks.call(mExecutor, Callable { + val metadata = File().setName(file.name) + val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) + mDriveService.files().update(fileId, metadata, mediaContent).execute() null }) } - fun getFilesInFolder(parentUid: String): Task { + fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { return Tasks.call(mExecutor, Callable { mDriveService.files().list() .setSpaces("drive") .setPageSize(1000) - .setQ("mimeType = '$GOOGLE_DRIVE_FILE_MIME_TYPE' and '$parentUid' in parents") + .setQ("mimeType = '$mimeType' and '$parentUid' in parents") .execute() }) } @@ -134,51 +125,4 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } } } - - /** - * Returns an [Intent] for opening the Storage Access Framework file picker. - */ - fun createFilePickerIntent(): Intent { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "text/plain" - - return intent - } - - /** - * Opens the file at the `uri` returned by a Storage Access Framework [Intent] - * created by [.createFilePickerIntent] using the given `contentResolver`. - */ - fun openFileUsingStorageAccessFramework( - contentResolver: ContentResolver, uri: Uri): Task> { - return Tasks.call(mExecutor, Callable { - // Retrieve the document's display name from its metadata. - var name: String = "" - contentResolver.query(uri, null, null, null, null)!!.use { cursor -> - if (cursor != null && cursor.moveToFirst()) { - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - name = cursor.getString(nameIndex) - } else { - throw IOException("Empty cursor returned for file.") - } - } - - // Read the document's contents as a String. - var content: String = "" - contentResolver.openInputStream(uri)!!.use { `is` -> - BufferedReader(InputStreamReader(`is`)).use { reader -> - val stringBuilder = StringBuilder() - var line: String = reader.readLine() - while (line != null) { - stringBuilder.append(line) - line = reader.readLine() - } - content = stringBuilder.toString() - } - } - - Pair.create(name, content) - }) - } } From 5679ab892b54398b3c4d49abe85cd12ca46fd411 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Apr 2019 18:05:29 +0100 Subject: [PATCH 007/134] [GoogleDrive Sync] Fixing image sync and deletion logic --- .../maubis/markdown/inliner/TextInliner.kt | 1 - .../quicknote/drive/GDriveRemoteDatabase.kt | 25 ++++++++ .../drive/GDriveRemoteImageFolder.kt | 35 +++++++---- .../quicknote/drive/GDriveRemoveFolder.kt | 5 +- .../quicknote/drive/GDriveServiceHelper.kt | 58 ++++++++++++++----- 5 files changed, 96 insertions(+), 28 deletions(-) diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt index 9eaefaea..0c82e619 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt @@ -30,7 +30,6 @@ class TextInliner(val text: String) { var index = 0 while (index < text.length) { val char = text.get(index) - Log.d("TextInliner", "char: " + char + " currentInline: " + currentInline.config.identifier()) if (currentInline.config.type() == MarkdownInlineType.INLINE_CODE && !currentInline.config.isEnd(text, index)) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index e7f2644f..1de206b2 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -5,6 +5,8 @@ import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.* import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.IFolderContainer +import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.INoteContainer import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.tag.ITagContainer @@ -75,6 +77,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat notesSync?.notifyingExistingIds(ids) } } + setupImageSync(rootFolderId) } } driveHelper?.getOrCreateDirectory(rootFolderId, "tags") { @@ -107,6 +110,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } } } + } + + private fun setupImageSync(rootFolderId: String) { driveHelper?.getOrCreateDirectory(rootFolderId, "images") { if (it !== null) { imageSync?.init(it) { @@ -148,6 +154,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } notesSync?.insert(note.uuid, note) + notifyImageIds(note) { + imageSync?.onInsert(it) + } } override fun insert(tag: ITagContainer) { @@ -169,6 +178,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } notesSync?.delete(note.uuid) + notifyImageIds(note) { + imageSync?.onRemove(it) + } } override fun remove(tag: ITagContainer) { @@ -195,6 +207,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } IRemoteDatabaseUtils.onRemoteInsert(context, note) + notifyImageIds(note) { + imageSync?.onInsert(it) + } } override fun onRemoteRemove(note: INoteContainer) { @@ -257,4 +272,14 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat IRemoteDatabaseUtils.onRemoteRemove(context, folder) } + fun notifyImageIds(note: INoteContainer, onImageUUID: (ImageUUID) -> Unit) { + val imageIds = FormatBuilder() + .getFormats(note.description()) + .filter { it.formatType == FormatType.IMAGE } + .map { it.text } + .toSet() + imageIds.forEach { + onImageUUID(ImageUUID(note.uuid(), it)) + } + } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 957c931c..14ac3472 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -1,9 +1,6 @@ package com.bijoysingh.quicknote.drive import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils.onRemoteInsert -import com.maubis.scarlet.base.export.remote.LAST_MODIFIED_ERROR_MARGIN import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -34,9 +31,6 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { fun init(fUid: String, onLoaded: () -> Unit) { folderUid = fUid - val lastScanKey = "${KEY_G_DRIVE_SYNC_LAST_SCAN}_$folderUid" - val lastScan = CoreConfig.instance.store().get(lastScanKey, 0L) - GlobalScope.launch(Dispatchers.IO) { helper.getFilesInFolder(folderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { val imageFiles = it.result?.files @@ -62,9 +56,15 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { } val remoteImages = imageFileIds.keys.toHashSet() - localImages.filter { !remoteImages.contains(it) }.forEach { + localImages.forEach { GlobalScope.launch { - onInsert(it) + val localFile = noteImagesFolder.getFile(it.noteUuid, it.imageUuid) + val hasLocalFile = localFile.exists() + val hasRemoteFile = remoteImages.contains(it) + when { + (hasLocalFile && !hasRemoteFile) -> onInsert(it) + (!hasLocalFile && hasRemoteFile) -> onRemoteInsert(it) + } } } } @@ -80,10 +80,11 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { } val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) - helper.createFile(folderId = folderUid, mimeType = GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { + val finalFileName = "${id.noteUuid}::${id.imageUuid}" + helper.createFile(folderId = folderUid, name = finalFileName, mimeType = GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { val createdFileId = it.result if (createdFileId !== null) { - helper.saveFile(createdFileId, imageFile) + helper.saveFile(createdFileId, finalFileName, imageFile) } } } @@ -94,10 +95,22 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { return } - + if (!imageFileIds.containsKey(id)) { + return + } + helper.removeFileOrFolder(imageFileIds[id] ?: INVALID_FILE_ID) + imageFileIds.remove(id) } fun onRemoteInsert(id: ImageUUID) { + if (!loaded.get()) { + return + } + if (!imageFileIds.containsKey(id)) { + return + } + val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) + helper.readFile(imageFileIds[id] ?: INVALID_FILE_ID, imageFile) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt index af68a9e9..feee005d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt @@ -136,7 +136,7 @@ class GDriveRemoteFolder( item is FirebaseFolder -> item.updateTimestamp else -> null } - helper.createFile(folderUid, modificationTime).addOnCompleteListener { + helper.createFile(folderUid, uuid, modificationTime).addOnCompleteListener { val createdFileId = it.result if (createdFileId !== null) { fileIds[uuid] = createdFileId @@ -153,8 +153,9 @@ class GDriveRemoteFolder( return } + helper.removeFileOrFolder(fileIds[uuid] ?: INVALID_FILE_ID) fileIds.remove(uuid) - helper.createFile(deletedFileId!!).addOnCompleteListener { + helper.createFile(deletedFileId!!, uuid).addOnCompleteListener { val removeFileId = it.result if (removeFileId !== null) { helper.saveFile(removeFileId, uuid, System.currentTimeMillis().toString()) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 37ebe3b4..64af374e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -1,5 +1,6 @@ package com.bijoysingh.quicknote.drive +import android.util.Log import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.api.client.http.ByteArrayContent @@ -9,6 +10,7 @@ import com.google.api.services.drive.Drive import com.google.api.services.drive.model.File import com.google.api.services.drive.model.FileList import java.io.BufferedReader +import java.io.FileOutputStream import java.io.InputStreamReader import java.util.concurrent.Callable import java.util.concurrent.Executors @@ -21,16 +23,31 @@ const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" +class ErrorCallable(val callable: Callable) : Callable { + override fun call(): T { + try { + return callable.call() + } catch (exception: Exception) { + Log.e("GoogleDrive", exception.message, exception) + throw exception + } + } +} + class GDriveServiceHelper(private val mDriveService: Drive) { private val mExecutor = Executors.newSingleThreadExecutor() - fun createFile(folderId: String, modificationTimeOverride: Long? = null, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { - return Tasks.call(mExecutor, Callable { + fun execute(callable: Callable): Task { + return Tasks.call(mExecutor, ErrorCallable(callable)) + } + + fun createFile(folderId: String, name: String, modificationTimeOverride: Long? = null, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { + return execute(Callable { val metadata = File() .setParents(listOf(folderId)) .setMimeType(mimeType) .setModifiedTime(DateTime(modificationTimeOverride ?: System.currentTimeMillis())) - .setName("file") + .setName(name) val googleFile = mDriveService.files().create(metadata).execute() googleFile?.id ?: INVALID_FILE_ID @@ -38,7 +55,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun createFolder(parentUid: String, folderName: String): Task { - return Tasks.call(mExecutor, Callable { + return execute(Callable { val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) .setName(folderName) @@ -52,10 +69,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun readFile(fileId: String): Task { - return Tasks.call(mExecutor, Callable { - val metadata = mDriveService.files().get(fileId).execute() - val name = metadata.name - + return execute(Callable { mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> BufferedReader(InputStreamReader(`is`)).use { reader -> val stringBuilder = StringBuilder() @@ -71,8 +85,17 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } + fun readFile(fileId: String, destinationFile: java.io.File): Task { + return execute(Callable { + destinationFile.parentFile.mkdirs() + val fileStream = FileOutputStream(destinationFile) + mDriveService.files().get(fileId).executeMediaAndDownloadTo(fileStream) + null + }) + } + fun saveFile(fileId: String, name: String, content: String): Task { - return Tasks.call(mExecutor, Callable { + return execute(Callable { val metadata = File().setName(name) val contentStream = ByteArrayContent.fromString("text/plain", content) mDriveService.files().update(fileId, metadata, contentStream).execute() @@ -80,9 +103,9 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun saveFile(fileId: String, file: java.io.File): Task { - return Tasks.call(mExecutor, Callable { - val metadata = File().setName(file.name) + fun saveFile(fileId: String, name: String, file: java.io.File): Task { + return execute(Callable { + val metadata = File().setName(name) val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) mDriveService.files().update(fileId, metadata, mediaContent).execute() null @@ -90,7 +113,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { - return Tasks.call(mExecutor, Callable { + return execute(Callable { mDriveService.files().list() .setSpaces("drive") .setPageSize(1000) @@ -104,7 +127,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { parentUid.isEmpty() -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name'" else -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name' and '$parentUid' in parents" } - return Tasks.call(mExecutor, Callable { + return execute(Callable { mDriveService.files().list() .setSpaces("drive") .setQ(query) @@ -112,6 +135,13 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } + fun removeFileOrFolder(fileUid: String): Task { + return execute(Callable { + mDriveService.files().delete(fileUid) + null + }) + } + fun getOrCreateDirectory(parentUid: String, name: String, onFolderId: (String?) -> Unit) { getFolderQuery(parentUid, name).addOnCompleteListener { getTask -> val fid = getTask.result?.files?.firstOrNull()?.id From e99af5a8ab379b2dd490b66052f47ba7b9d76368 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 29 Apr 2019 22:59:19 +0100 Subject: [PATCH 008/134] [GDrive] Fixing logic to upload lazily --- .../quicknote/drive/GDriveRemoteDatabase.kt | 121 +++++-------- .../quicknote/drive/GDriveRemoteFolder.kt | 125 +++++++++++++ .../drive/GDriveRemoteImageFolder.kt | 77 +++----- .../quicknote/drive/GDriveRemoveFolder.kt | 165 ------------------ .../quicknote/drive/GDriveServiceHelper.kt | 89 ++++++++-- 5 files changed, 263 insertions(+), 314 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt delete mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 1de206b2..eab6fa0f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,22 +1,27 @@ package com.bijoysingh.quicknote.drive import android.content.Context -import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.* import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.IFolderContainer import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.INoteContainer -import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.tag.ITagContainer import com.maubis.scarlet.base.database.remote.IRemoteDatabase import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils -import com.maubis.scarlet.base.note.getImageIds import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference +const val FOLDER_NAME_IMAGES = "images" +const val FOLDER_NAME_NOTES = "notes" +const val FOLDER_NAME_TAGS = "tags" +const val FOLDER_NAME_FOLDERS = "folders" +const val FOLDER_NAME_DELETED_NOTES = "deleted:notes" +const val FOLDER_NAME_DELETED_TAGS = "deleted:tags" +const val FOLDER_NAME_DELETED_FOLDERS = "deleted:folders" + class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDatabase { private var isValidController: Boolean = true @@ -33,24 +38,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat isValidController = true driveHelper = helper - notesSync = GDriveRemoteFolder( - FirebaseNote::class.java, - helper, - { it -> CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() }, - { it -> onRemoteInsert(it) }, - { it -> onRemoteRemove(FirebaseNote(it, "", 0L, 0L, 0, NoteState.DEFAULT.name, "", false, false, "")) }) - tagsSync = GDriveRemoteFolder( - FirebaseTag::class.java, - helper, - { it -> CoreConfig.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() }, - { it -> onRemoteInsert(it) }, - { it -> onRemoteRemove(FirebaseTag(it, "")) }) - foldersSync = GDriveRemoteFolder( - FirebaseFolder::class.java, - helper, - { it -> CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() }, - { it -> onRemoteInsert(it) }, - { it -> onRemoteRemove(FirebaseFolder(it, "", 0L, 0L, 0)) }) + notesSync = GDriveRemoteFolder(helper) { CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() } + tagsSync = GDriveRemoteFolder(helper) { CoreConfig.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() } + foldersSync = GDriveRemoteFolder(helper) { CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() } imageSync = GDriveRemoteImageFolder(helper) GlobalScope.launch { @@ -63,68 +53,37 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } } - fun onRootFolderLoaded(rootFolderId: String) { - driveHelper?.getOrCreateDirectory(rootFolderId, "notes") { - if (it !== null) { - notesSync?.init(it) { - if (!sGDriveFirstSyncNote) { - CoreConfig.instance.notesDatabase().getAll().forEach { - gDrive?.insert(it.getFirebaseNote()) - } - sGDriveFirstSyncNote = true - } else { - val ids = CoreConfig.instance.notesDatabase().getUUIDs() - notesSync?.notifyingExistingIds(ids) - } - } - setupImageSync(rootFolderId) - } - } - driveHelper?.getOrCreateDirectory(rootFolderId, "tags") { - if (it !== null) { - tagsSync?.init(it) { - if (!sGDriveFirstSyncTag) { - CoreConfig.instance.tagsDatabase().getAll().forEach { - gDrive?.insert(it.getFirebaseTag()) - } - sGDriveFirstSyncTag = true - } else { - val ids = CoreConfig.instance.tagsDatabase().getUUIDs() - tagsSync?.notifyingExistingIds(ids) - } - } - } - } - driveHelper?.getOrCreateDirectory(rootFolderId, "folders") { - if (it !== null) { - foldersSync?.init(it) { - if (!sGDriveFirstSyncFolder) { - CoreConfig.instance.foldersDatabase().getAll().forEach { - gDrive?.insert(it.getFirebaseFolder()) - } - sGDriveFirstSyncFolder = true - } else { - val ids = CoreConfig.instance.foldersDatabase().getUUIDs() - foldersSync?.notifyingExistingIds(ids) - } - } - } + fun initSubRootFolder(folderName: String, folderId: String) { + when (folderName) { + FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) {} + FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) {} + FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) {} + FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) {} + FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) {} + FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) {} + FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) {} } } - private fun setupImageSync(rootFolderId: String) { - driveHelper?.getOrCreateDirectory(rootFolderId, "images") { - if (it !== null) { - imageSync?.init(it) { - val imageIds = emptySet().toMutableSet() - CoreConfig.instance.notesDatabase().getAll().map { - Pair(it.uuid, it.getImageIds()) - }.forEach { idImagesPair -> - idImagesPair.second.forEach { imageId -> - imageIds.add(ImageUUID(idImagesPair.first, imageId)) + fun onRootFolderLoaded(rootFolderId: String) { + val expectedFolders = listOf( + FOLDER_NAME_IMAGES, + FOLDER_NAME_NOTES, FOLDER_NAME_TAGS, FOLDER_NAME_FOLDERS, + FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS) + driveHelper?.getSubRootFolders(rootFolderId, expectedFolders)?.addOnCompleteListener { + val fileIds = it.result?.files ?: emptyList() + val existingFiles = fileIds.map { it.name } + fileIds.forEach { + GlobalScope.launch { initSubRootFolder(it.name, it.id) } + } + expectedFolders.forEach { expectedFolder -> + if (!existingFiles.contains(expectedFolder)) { + driveHelper?.createFolder(rootFolderId, expectedFolder)?.addOnCompleteListener { fileIdTask -> + val fileId = fileIdTask.result + if (fileId !== null) { + GlobalScope.launch { initSubRootFolder(expectedFolder, fileId) } } } - imageSync?.notifyingExistingIds(imageIds) } } } @@ -155,7 +114,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } notesSync?.insert(note.uuid, note) notifyImageIds(note) { - imageSync?.onInsert(it) + imageSync?.insert(it) } } @@ -179,7 +138,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } notesSync?.delete(note.uuid) notifyImageIds(note) { - imageSync?.onRemove(it) + imageSync?.delete(it) } } @@ -208,7 +167,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } IRemoteDatabaseUtils.onRemoteInsert(context, note) notifyImageIds(note) { - imageSync?.onInsert(it) + imageSync?.insert(it) } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt new file mode 100644 index 00000000..466db393 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -0,0 +1,125 @@ +package com.bijoysingh.quicknote.drive + +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean + +const val KEY_G_DRIVE_SYNC_LAST_SCAN = "drive_g_folder_sync_last_sync" +const val G_DRIVE_LAST_MODIFIED_ERROR_MARGIN = 7 * 1000 * 60 * 60 * 24L +const val G_DRIVE_LAST_MODIFIED_UPDATE_ERROR_MARGIN = 1000 * 60 * 60 * 1L + +class GDriveRemoteFolder( + val helper: GDriveServiceHelper, + val uuidToObject: (String) -> T?) { + + var contentLoading = AtomicBoolean(true) + var contentFolderUid: String = INVALID_FILE_ID + var contentPendingActions = emptySet().toMutableSet() + val contentFiles = emptyMap().toMutableMap() + + var deletedLoading = AtomicBoolean(true) + var deletedFolderUid: String = INVALID_FILE_ID + var deletedPendingActions = emptySet().toMutableSet() + val deletedFiles = emptyMap().toMutableMap() + + fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { + GlobalScope.launch(Dispatchers.IO) { + contentLoading.set(true) + contentFolderUid = fUid + helper.getFilesInFolder(contentFolderUid).addOnCompleteListener { + val files = it.result?.files ?: emptyList() + val localFileIds = emptyMap().toMutableMap() + files.forEach { file -> + localFileIds[file.name] = file.id + } + contentFiles.clear() + contentFiles.putAll(localFileIds) + contentLoading.set(false) + + GlobalScope.launch { executeInsertPendingActions() } + GlobalScope.launch { onLoaded() } + } + } + } + + fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { + GlobalScope.launch(Dispatchers.IO) { + deletedLoading.set(true) + deletedFolderUid = fUid + helper.getFilesInFolder(deletedFolderUid).addOnCompleteListener { + val files = it.result?.files ?: emptyList() + val localFileIds = emptyMap().toMutableMap() + files.forEach { file -> + localFileIds[file.name] = file.id + } + deletedFiles.clear() + deletedFiles.putAll(localFileIds) + deletedLoading.set(false) + + GlobalScope.launch { executeDeletePendingActions() } + GlobalScope.launch { onLoaded() } + } + } + } + + fun executeInsertPendingActions() { + contentPendingActions.forEach { uuid -> + GlobalScope.launch { + val item = uuidToObject(uuid) + if (item !== null) { + insert(uuid, item) + } + } + } + } + + fun executeDeletePendingActions() { + deletedPendingActions.forEach { + GlobalScope.launch { delete(it) } + } + } + + fun insert(uuid: String, item: T) { + if (contentLoading.get()) { + contentPendingActions.add(uuid) + return + } + + try { + val data = Gson().toJson(item) + val fileId = contentFiles[uuid] + if (fileId !== null) { + helper.saveFile(fileId, uuid, data).addOnCompleteListener { + helper.updateLastModifiedTime(contentFolderUid) + } + return + } + + helper.createFileWithData(contentFolderUid, uuid, data).addOnCompleteListener { + contentFiles[uuid] = it.result ?: INVALID_FILE_ID + helper.updateLastModifiedTime(contentFolderUid) + } + } catch (exception: Exception) { + } + } + + fun delete(uuid: String) { + if (deletedLoading.get() || contentLoading.get()) { + deletedPendingActions.add(uuid) + return + } + + val existingFileUid = contentFiles[uuid] + if (existingFileUid !== null) { + helper.removeFileOrFolder(existingFileUid) + contentFiles.remove(uuid) + } + + helper.createFileWithData(deletedFolderUid, uuid).addOnCompleteListener { + deletedFiles[uuid] = it.result ?: INVALID_FILE_ID + helper.updateLastModifiedTime(deletedFolderUid) + } + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 14ac3472..6938ba33 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -24,15 +24,17 @@ data class ImageUUID( class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { - val loaded = AtomicBoolean(false) - val imageFileIds = emptyMap().toMutableMap() - val pendingActions = emptyMap().toMutableMap() - var folderUid: String = INVALID_FILE_ID + val contentLoading = AtomicBoolean(true) + var contentFolderUid: String = INVALID_FILE_ID + val contentFiles = emptyMap().toMutableMap() - fun init(fUid: String, onLoaded: () -> Unit) { - folderUid = fUid + val contentPendingActions = emptySet().toMutableSet() + val deletedPendingActions = emptySet().toMutableSet() + + fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { + contentFolderUid = fUid GlobalScope.launch(Dispatchers.IO) { - helper.getFilesInFolder(folderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { + helper.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { val imageFiles = it.result?.files if (imageFiles !== null) { imageFiles.forEach { imageFile -> @@ -40,77 +42,44 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { if (components.size == 2) { val noteUuid = components[0] val imageId = components[1] - imageFileIds[ImageUUID(noteUuid, imageId)] = imageFile.id + contentFiles[ImageUUID(noteUuid, imageId)] = imageFile.id } } - loaded.set(true) + contentLoading.set(false) GlobalScope.launch { onLoaded() } } } } } - fun notifyingExistingIds(localImages: Set) { - if (!loaded.get()) { - return - } - - val remoteImages = imageFileIds.keys.toHashSet() - localImages.forEach { - GlobalScope.launch { - val localFile = noteImagesFolder.getFile(it.noteUuid, it.imageUuid) - val hasLocalFile = localFile.exists() - val hasRemoteFile = remoteImages.contains(it) - when { - (hasLocalFile && !hasRemoteFile) -> onInsert(it) - (!hasLocalFile && hasRemoteFile) -> onRemoteInsert(it) - } - } - } - } - - fun onInsert(id: ImageUUID) { - if (!loaded.get()) { - pendingActions.put(id, "insert") + fun insert(id: ImageUUID) { + if (contentLoading.get()) { + contentPendingActions.add(id) return } - if (imageFileIds.containsKey(id)) { + if (contentFiles.containsKey(id)) { return } val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) val finalFileName = "${id.noteUuid}::${id.imageUuid}" - helper.createFile(folderId = folderUid, name = finalFileName, mimeType = GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { - val createdFileId = it.result - if (createdFileId !== null) { - helper.saveFile(createdFileId, finalFileName, imageFile) - } + helper.createFileWithData(contentFolderUid, finalFileName, imageFile).addOnCompleteListener { + helper.updateLastModifiedTime(contentFolderUid) } } - fun onRemove(id: ImageUUID) { - if (!loaded.get()) { - pendingActions.put(id, "remove") - return - } - - if (!imageFileIds.containsKey(id)) { + fun delete(id: ImageUUID) { + if (contentLoading.get()) { + deletedPendingActions.add(id) return } - helper.removeFileOrFolder(imageFileIds[id] ?: INVALID_FILE_ID) - imageFileIds.remove(id) - } - fun onRemoteInsert(id: ImageUUID) { - if (!loaded.get()) { + if (!contentFiles.containsKey(id)) { return } - if (!imageFileIds.containsKey(id)) { - return - } - val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) - helper.readFile(imageFileIds[id] ?: INVALID_FILE_ID, imageFile) + helper.removeFileOrFolder(contentFiles[id] ?: INVALID_FILE_ID) + contentFiles.remove(id) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt deleted file mode 100644 index feee005d..00000000 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoveFolder.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.bijoysingh.quicknote.drive - -import com.bijoysingh.quicknote.firebase.data.FirebaseFolder -import com.bijoysingh.quicknote.firebase.data.FirebaseNote -import com.google.gson.Gson -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.export.remote.LAST_MODIFIED_ERROR_MARGIN -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.util.concurrent.atomic.AtomicBoolean - -const val KEY_G_DRIVE_SYNC_LAST_SCAN = "drive_g_folder_sync_last_sync" - -class GDriveRemoteFolder( - val klass: Class, - val helper: GDriveServiceHelper, - val uuidToObject: (String) -> T?, - val onRemoteInsert: (T) -> Unit, - val onRemoteDelete: (String) -> Unit) { - - // Mapping from uuid => fileIds - var deletedFileId: String? = null - val loaded = AtomicBoolean(false) - val fileIds = emptyMap().toMutableMap() - val pendingActions = emptyMap().toMutableMap() - val deletedUuids = HashSet() - var folderUid: String = INVALID_FILE_ID - - fun init(fUid: String, onLoaded: () -> Unit) { - folderUid = fUid - val lastScanKey = "${KEY_G_DRIVE_SYNC_LAST_SCAN}_$folderUid" - val lastScan = CoreConfig.instance.store().get(lastScanKey, 0L) - - GlobalScope.launch(Dispatchers.IO) { - helper.getFilesInFolder(folderUid).addOnCompleteListener { - val files = it.result?.files - if (files !== null) { - files.forEach { file -> - fileIds[file.name] = file.id - if (file.mimeType == GOOGLE_DRIVE_FILE_MIME_TYPE - && (file.modifiedTime?.value ?: System.currentTimeMillis() > lastScan - LAST_MODIFIED_ERROR_MARGIN)) { - helper.readFile(file.id).addOnCompleteListener { - val data = it.result - if (data !== null) { - try { - val item = Gson().fromJson(data, klass) - if (item !== null) { - onRemoteInsert(item) - } - } catch (exception: Exception) { - } - } - } - } - } - loaded.set(true) - removePendingActions() - scanDeletedFiles() - GlobalScope.launch { onLoaded() } - } - } - } - } - - fun scanDeletedFiles() { - helper.getOrCreateDirectory(folderUid, "delete") { - deletedFileId = it - if (deletedFileId !== null) { - helper.getFilesInFolder(deletedFileId!!).addOnCompleteListener { - val files = it.result?.files - if (files !== null) { - files.forEach { file -> - if (file.mimeType == GOOGLE_DRIVE_FILE_MIME_TYPE) { - deletedUuids.add(file.name) - onRemoteDelete(file.name) - } - } - } - } - removePendingActions() - } - } - } - - fun removePendingActions() { - val tPendingActions = emptyMap().toMutableMap() - tPendingActions.putAll(pendingActions) - pendingActions.clear() - - GlobalScope.launch { - tPendingActions.forEach { - when (it.value) { - "insert" -> { - val item = uuidToObject(it.key) - if (item !== null) { - insert(it.key, item) - } - } - "delete" -> delete(it.key) - } - } - } - } - - fun notifyingExistingIds(ids: List) { - if (!loaded.get()) { - return - } - - val uuids = fileIds.keys.toHashSet() - ids.filter { !uuids.contains(it) }.forEach { - val item = uuidToObject(it) - if (item !== null) { - insert(it, item) - } - } - } - - fun insert(uuid: String, item: T) { - if (!loaded.get()) { - pendingActions[uuid] = "insert" - return - } - - try { - val data = Gson().toJson(item) - val fileId = fileIds.get(uuid) - if (fileId !== null) { - helper.saveFile(fileId, uuid, data) - return - } - - val modificationTime = when { - item is FirebaseNote -> item.updateTimestamp - item is FirebaseFolder -> item.updateTimestamp - else -> null - } - helper.createFile(folderUid, uuid, modificationTime).addOnCompleteListener { - val createdFileId = it.result - if (createdFileId !== null) { - fileIds[uuid] = createdFileId - helper.saveFile(createdFileId, uuid, data) - } - } - } catch (exception: Exception) { - } - } - - fun delete(uuid: String) { - if (!loaded.get() || deletedFileId === null) { - pendingActions[uuid] = "delete" - return - } - - helper.removeFileOrFolder(fileIds[uuid] ?: INVALID_FILE_ID) - fileIds.remove(uuid) - helper.createFile(deletedFileId!!, uuid).addOnCompleteListener { - val removeFileId = it.result - if (removeFileId !== null) { - helper.saveFile(removeFileId, uuid, System.currentTimeMillis().toString()) - } - } - } -} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 64af374e..2dd17a99 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -9,9 +9,11 @@ import com.google.api.client.util.DateTime import com.google.api.services.drive.Drive import com.google.api.services.drive.model.File import com.google.api.services.drive.model.FileList +import com.maubis.scarlet.base.config.CoreConfig import java.io.BufferedReader import java.io.FileOutputStream import java.io.InputStreamReader +import java.util.* import java.util.concurrent.Callable import java.util.concurrent.Executors @@ -23,6 +25,13 @@ const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" +const val STORE_KEY_G_DRIVE_LAST_MODIFICATION_TIME = "store_key_g_drive_last_modification_time" +var sGDriveLastModificationTime: Long + get() = CoreConfig.instance.store().get(STORE_KEY_G_DRIVE_LAST_MODIFICATION_TIME, 0L) + set(value) = CoreConfig.instance.store().put(STORE_KEY_G_DRIVE_LAST_MODIFICATION_TIME, value) + +var updateCheckerFileId: String? = null + class ErrorCallable(val callable: Callable) : Callable { override fun call(): T { try { @@ -34,22 +43,48 @@ class ErrorCallable(val callable: Callable) : Callable { } } +fun getTrueCurrentTime(): Long { + var calendar: Calendar = Calendar.getInstance() + try { + calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + } catch (exception: Exception) { + } + return calendar.timeInMillis +} + class GDriveServiceHelper(private val mDriveService: Drive) { - private val mExecutor = Executors.newSingleThreadExecutor() + private val mExecutor = Executors.newFixedThreadPool(8) + private val mSerialExecutor = Executors.newSingleThreadExecutor() fun execute(callable: Callable): Task { return Tasks.call(mExecutor, ErrorCallable(callable)) } - fun createFile(folderId: String, name: String, modificationTimeOverride: Long? = null, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { + fun createFileWithData(folderId: String, name: String, content: String = ""): Task { + val currentTime = getTrueCurrentTime() + val contentToSave = if (content.isEmpty()) currentTime.toString() else content return execute(Callable { val metadata = File() .setParents(listOf(folderId)) - .setMimeType(mimeType) - .setModifiedTime(DateTime(modificationTimeOverride ?: System.currentTimeMillis())) + .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) + .setModifiedTime(DateTime(currentTime)) .setName(name) + val contentStream = ByteArrayContent.fromString("text/plain", contentToSave) + val googleFile = mDriveService.files().create(metadata, contentStream).execute() + googleFile?.id ?: INVALID_FILE_ID + }) + } - val googleFile = mDriveService.files().create(metadata).execute() + fun createFileWithData(folderId: String, name: String, file: java.io.File): Task { + val currentTime = getTrueCurrentTime() + return execute(Callable { + val metadata = File() + .setParents(listOf(folderId)) + .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) + .setModifiedTime(DateTime(currentTime)) + .setName(name) + val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) + val googleFile = mDriveService.files().create(metadata, mediaContent).execute() googleFile?.id ?: INVALID_FILE_ID }) } @@ -58,6 +93,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { return execute(Callable { val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) + .setModifiedTime(DateTime(getTrueCurrentTime())) .setName(folderName) if (!parentUid.isEmpty()) { metadata.parents = listOf(parentUid) @@ -103,15 +139,6 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun saveFile(fileId: String, name: String, file: java.io.File): Task { - return execute(Callable { - val metadata = File().setName(name) - val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) - mDriveService.files().update(fileId, metadata, mediaContent).execute() - null - }) - } - fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { return execute(Callable { mDriveService.files().list() @@ -135,6 +162,19 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } + fun getSubRootFolders(parentUid: String, names: List): Task { + var nameQueryBuilder = "name = '${names[0]}'" + names.subList(1, names.lastIndex).forEach { + nameQueryBuilder += " or name = '$it'" + } + return execute(Callable { + mDriveService.files().list() + .setSpaces("drive") + .setQ("mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and ($nameQueryBuilder) and '$parentUid' in parents") + .execute() + }) + } + fun removeFileOrFolder(fileUid: String): Task { return execute(Callable { mDriveService.files().delete(fileUid) @@ -155,4 +195,25 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } } } + + fun getLastUpdateTime(): Task { + val folderId = updateCheckerFileId + return Tasks.call(mSerialExecutor, ErrorCallable(Callable { + when { + (folderId === null) -> throw RuntimeException("Folder not set") + else -> mDriveService.files().get(folderId).execute() + } + })) + } + + fun updateLastModifiedTime(folderUid: String): Long { + val currentTime = getTrueCurrentTime() + Tasks.call(mSerialExecutor, ErrorCallable(Callable { + val metadata = File() + .setModifiedTime(DateTime(currentTime)) + .setModifiedByMeTime(DateTime(currentTime)) + mDriveService.files().update(folderUid, metadata).execute() + })) + return currentTime + } } From 54b22181651fee07678fd29b77ed3f545cff9fee Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 2 May 2019 23:33:09 +0100 Subject: [PATCH 009/134] [GDrive] Multiple syncing optimisations for drive sync --- base/build.gradle | 5 +- .../com/maubis/scarlet/base/MainActivity.kt | 9 + .../maubis/scarlet/base/config/CoreConfig.kt | 2 + .../scarlet/base/config/MaterialNoteConfig.kt | 2 + base/src/main/res/layout/activity_main.xml | 33 +++ base/src/main/res/values/strings.xml | 1 + scarlet/build.gradle | 2 + .../quicknote/database/GDriveUploadData.kt | 53 +++++ .../database/GDriveUploadDatabase.kt | 19 ++ .../quicknote/drive/GDriveLoginActivity.kt | 40 +++- .../quicknote/drive/GDriveRemoteDatabase.kt | 215 ++++++++++++++++-- .../quicknote/drive/GDriveRemoteFolder.kt | 74 ++++-- .../drive/GDriveRemoteImageFolder.kt | 4 +- .../quicknote/drive/GDriveServiceHelper.kt | 67 ++---- .../firebase/support/ScarletAuthenticator.kt | 4 +- .../quicknote/scarlet/ScarletConfig.kt | 9 + .../quicknote/scarlet/ScarletFolderActor.kt | 4 +- .../quicknote/scarlet/ScarletNoteActor.kt | 4 +- .../quicknote/scarlet/ScarletTagActor.kt | 4 +- 19 files changed, 454 insertions(+), 97 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt diff --git a/base/build.gradle b/base/build.gradle index 233f9ac3..411a1250 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -54,9 +54,8 @@ dependencies { implementation 'com.google.android:flexbox:0.3.2' def room_version = "1.1.1" - implementation "android.arch.persistence.room:runtime:$room_version" - implementation "android.arch.persistence.room:testing:$room_version" - annotationProcessor "android.arch.persistence.room:compiler:$room_version" + api "android.arch.persistence.room:runtime:$room_version" + api "android.arch.persistence.room:testing:$room_version" kapt "android.arch.persistence.room:compiler:$room_version" implementation 'com.github.ajalt.reprint:core:3.2.0@aar' diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 8d404868..fe5cb1b3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -328,6 +328,15 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { CoreConfig.instance.startListener(this) setupData() registerNoteReceiver() + + topSyncingLayout.visibility = View.VISIBLE + GlobalScope.launch { + CoreConfig.instance.resyncDrive { + GlobalScope.launch(Dispatchers.Main) { + topSyncingLayout.visibility = View.GONE + } + } + } } fun resetAndSetupData() { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index a0f7cdb8..4391965f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -62,6 +62,8 @@ abstract class CoreConfig(context: Context) { abstract fun imageCache(): ImageCache + abstract fun resyncDrive(onSyncCompleted: () -> Unit) + companion object { lateinit var instance: CoreConfig val notesDb get() = instance.notesDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 4ea9b0ab..95e28d21 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -66,4 +66,6 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun store(): Store = store override fun imageCache(): ImageCache = imageCache + + override fun resyncDrive(onSyncCompleted: () -> Unit) = onSyncCompleted() } \ No newline at end of file diff --git a/base/src/main/res/layout/activity_main.xml b/base/src/main/res/layout/activity_main.xml index 21eef020..3b6b7864 100644 --- a/base/src/main/res/layout/activity_main.xml +++ b/base/src/main/res/layout/activity_main.xml @@ -42,6 +42,39 @@ android:layout_height="wrap_content" /> + + + + + + + + Help and Common Questions Find help on how to use the features in the app + Syncing diff --git a/scarlet/build.gradle b/scarlet/build.gradle index 024e01f7..b7968bb6 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'com.github.zellius.shortcut-helper' +apply plugin: 'kotlin-kapt' shortcutHelper.filePath = '../shortcuts.xml' @@ -53,6 +54,7 @@ android { dependencies { api project(':base') + kapt "android.arch.persistence.room:compiler:1.1.1" implementation 'com.android.volley:volley:1.1.0' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt new file mode 100644 index 00000000..3329a141 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -0,0 +1,53 @@ +package com.bijoysingh.quicknote.database + +import android.arch.persistence.room.* + +enum class GDriveDataType { + NOTE, + TAG, + FOLDER +} + +@Entity(tableName = "gdrive_upload", indices = [Index("uuid")]) +class GDriveUploadData { + @PrimaryKey(autoGenerate = true) + var uid: Int = 0 + + var uuid: String = "" + + var type: String = "" + + var fileId: String = "" + + var lastUpdateTimestamp: Long = 0L + + var localStateDeleted: Boolean = false + + var gDriveUpdateTimestamp: Long = 0L + + var gDriveStateDeleted: Boolean = false + + @Ignore + fun save(dao: GDriveUploadDataDao) { + val id = dao.insert(this) + uid = if (uid == 0) id.toInt() else uid + } +} + +@Dao +interface GDriveUploadDataDao { + @get:Query("SELECT * FROM gdrive_upload") + val all: List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(note: GDriveUploadData): Long + + @Delete + fun delete(note: GDriveUploadData) + + @Query("SELECT * FROM gdrive_upload WHERE uid = :uid LIMIT 1") + fun getByID(uid: Int): GDriveUploadData? + + @Query("SELECT * FROM gdrive_upload WHERE uuid = :uuid AND type = :type LIMIT 1") + fun getByUUID(type: String, uuid: String): GDriveUploadData? +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt new file mode 100644 index 00000000..eb9c6963 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt @@ -0,0 +1,19 @@ +package com.bijoysingh.quicknote.database + +import android.arch.persistence.room.Database +import android.arch.persistence.room.Room +import android.arch.persistence.room.RoomDatabase +import android.content.Context + +var gDriveDatabase: GDriveUploadDataDao? = null +fun genGDriveUploadDatabase(context: Context): GDriveUploadDataDao? { + if (gDriveDatabase === null) { + gDriveDatabase = Room.databaseBuilder(context, GDriveUploadDatabase::class.java, "google_drive_db").build().drive() + } + return gDriveDatabase +} + +@Database(entities = [GDriveUploadData::class], version = 1) +abstract class GDriveUploadDatabase : RoomDatabase() { + abstract fun drive(): GDriveUploadDataDao +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 4bc1b7d4..986298bb 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadData import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -20,10 +22,11 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.export.support.KEY_EXTERNAL_FOLDER_SYNC_ENABLED import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.android.synthetic.main.gdrive_login.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -150,7 +153,40 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) gDrive?.init(mDriveServiceHelper!!) - setButton(false) + GlobalScope.launch { + val database = gDrive?.gDriveDatabase + if (database === null) { + return@launch + } + CoreConfig.instance.notesDatabase().getAll().forEach { + val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.NOTE.name, it.uuid) + ?: GDriveUploadData() + existing.apply { + uuid = it.uuid + type = GDriveDataType.NOTE.name + save(database) + } + } + CoreConfig.instance.tagsDatabase().getAll().forEach { + val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.TAG.name, it.uuid) + ?: GDriveUploadData() + existing.apply { + uuid = it.uuid + type = GDriveDataType.TAG.name + save(database) + } + } + CoreConfig.instance.foldersDatabase().getAll().forEach { + val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.FOLDER.name, it.uuid) + ?: GDriveUploadData() + existing.apply { + uuid = it.uuid + type = GDriveDataType.FOLDER.name + save(database) + } + } + finish() + } } override fun onConnectionFailed(p0: ConnectionResult) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index eab6fa0f..ba340f0a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,6 +1,11 @@ package com.bijoysingh.quicknote.drive import android.content.Context +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadData +import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.bijoysingh.quicknote.database.genGDriveUploadDatabase import com.bijoysingh.quicknote.firebase.data.* import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.IFolderContainer @@ -18,12 +23,14 @@ const val FOLDER_NAME_IMAGES = "images" const val FOLDER_NAME_NOTES = "notes" const val FOLDER_NAME_TAGS = "tags" const val FOLDER_NAME_FOLDERS = "folders" -const val FOLDER_NAME_DELETED_NOTES = "deleted:notes" -const val FOLDER_NAME_DELETED_TAGS = "deleted:tags" -const val FOLDER_NAME_DELETED_FOLDERS = "deleted:folders" +const val FOLDER_NAME_DELETED_NOTES = "deleted_notes" +const val FOLDER_NAME_DELETED_TAGS = "deleted_tags" +const val FOLDER_NAME_DELETED_FOLDERS = "deleted_folders" class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDatabase { + var gDriveDatabase: GDriveUploadDataDao? = null + private var isValidController: Boolean = true private var driveHelper: GDriveServiceHelper? = null @@ -35,12 +42,24 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat override fun init(userId: String) {} fun init(helper: GDriveServiceHelper) { + val context = weakContext.get() + if (context === null) { + return + } + isValidController = true driveHelper = helper + gDriveDatabase = genGDriveUploadDatabase(context) - notesSync = GDriveRemoteFolder(helper) { CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() } - tagsSync = GDriveRemoteFolder(helper) { CoreConfig.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() } - foldersSync = GDriveRemoteFolder(helper) { CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() } + notesSync = GDriveRemoteFolder(GDriveDataType.NOTE, gDriveDatabase!!, helper) { + CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() + } + tagsSync = GDriveRemoteFolder(GDriveDataType.TAG, gDriveDatabase!!, helper) { + CoreConfig.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() + } + foldersSync = GDriveRemoteFolder(GDriveDataType.FOLDER, gDriveDatabase!!, helper) { + CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() + } imageSync = GDriveRemoteImageFolder(helper) GlobalScope.launch { @@ -55,9 +74,30 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat fun initSubRootFolder(folderName: String, folderId: String) { when (folderName) { - FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) {} - FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) {} - FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) {} + FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { + if (!sGDriveFirstSyncNote) { + CoreConfig.instance.notesDatabase().getAll().forEach { + gDrive?.notifyInsert(it.getFirebaseNote()) + } + sGDriveFirstSyncNote = true + } + } + FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) { + if (!sGDriveFirstSyncTag) { + CoreConfig.instance.tagsDatabase().getAll().forEach { + gDrive?.notifyInsert(it.getFirebaseTag()) + } + sGDriveFirstSyncTag = true + } + } + FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { + if (!sGDriveFirstSyncFolder) { + CoreConfig.instance.foldersDatabase().getAll().forEach { + gDrive?.notifyInsert(it.getFirebaseFolder()) + } + sGDriveFirstSyncFolder = true + } + } FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) {} FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) {} FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) {} @@ -66,10 +106,11 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } fun onRootFolderLoaded(rootFolderId: String) { - val expectedFolders = listOf( - FOLDER_NAME_IMAGES, - FOLDER_NAME_NOTES, FOLDER_NAME_TAGS, FOLDER_NAME_FOLDERS, - FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS) + createFolders(rootFolderId, listOf(FOLDER_NAME_IMAGES, FOLDER_NAME_NOTES, FOLDER_NAME_TAGS, FOLDER_NAME_FOLDERS)) + createFolders(rootFolderId, listOf(FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS)) + } + + fun createFolders(rootFolderId: String, expectedFolders: List) { driveHelper?.getSubRootFolders(rootFolderId, expectedFolders)?.addOnCompleteListener { val fileIds = it.result?.files ?: emptyList() val existingFiles = fileIds.map { it.name } @@ -79,9 +120,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat expectedFolders.forEach { expectedFolder -> if (!existingFiles.contains(expectedFolder)) { driveHelper?.createFolder(rootFolderId, expectedFolder)?.addOnCompleteListener { fileIdTask -> - val fileId = fileIdTask.result - if (fileId !== null) { - GlobalScope.launch { initSubRootFolder(expectedFolder, fileId) } + val file = fileIdTask.result + if (file !== null) { + GlobalScope.launch { initSubRootFolder(expectedFolder, file.id) } } } } @@ -108,6 +149,140 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } } + fun notifyInsert(note: INoteContainer) { + if (!isValidController || note !is FirebaseNote) { + return + } + localDatabaseUpdate(GDriveDataType.NOTE, note.uuid) + } + + fun notifyInsert(tag: ITagContainer) { + if (!isValidController || tag !is FirebaseTag) { + return + } + localDatabaseUpdate(GDriveDataType.TAG, tag.uuid) + } + + fun notifyInsert(folder: IFolderContainer) { + if (!isValidController || folder !is FirebaseFolder) { + return + } + localDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid) + } + + fun notifyRemove(note: INoteContainer) { + if (!isValidController || note !is FirebaseNote) { + return + } + localDatabaseUpdate(GDriveDataType.NOTE, note.uuid, true) + } + + fun notifyRemove(tag: ITagContainer) { + if (!isValidController || tag !is FirebaseTag) { + return + } + localDatabaseUpdate(GDriveDataType.TAG, tag.uuid, true) + } + + fun notifyRemove(folder: IFolderContainer) { + if (!isValidController || folder !is FirebaseFolder) { + return + } + localDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid, true) + } + + fun resync(onSyncCompleted: () -> Unit) { + if (!isValidController) { + onSyncCompleted() + return + } + + GlobalScope.launch { + gDriveDatabase?.all?.forEach { + val sameDelete = it.localStateDeleted == it.gDriveStateDeleted + val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp + if (!sameUpdateTime) { + var note: FirebaseNote? = null + var tag: FirebaseTag? = null + var folder: FirebaseFolder? = null + when (it.type) { + GDriveDataType.NOTE.name -> note = CoreConfig.instance.notesDatabase().getByUUID(it.uuid)?.getFirebaseNote() + GDriveDataType.TAG.name -> tag = CoreConfig.instance.tagsDatabase().getByUUID(it.uuid)?.getFirebaseTag() + GDriveDataType.FOLDER.name -> folder = CoreConfig.instance.foldersDatabase().getByUUID(it.uuid)?.getFirebaseFolder() + } + + when { + sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> { + when { + note !== null -> insert(note) + tag !== null -> insert(tag) + folder !== null -> insert(folder) + } + } + sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> { + when { + note !== null -> onRemoteInsert(note) + tag !== null -> onRemoteInsert(tag) + folder !== null -> onRemoteInsert(folder) + } + } + !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> { + when { + note !== null -> remove(note) + tag !== null -> remove(tag) + folder !== null -> remove(folder) + } + } + !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> { + when { + note !== null -> onRemoteRemove(note) + tag !== null -> onRemoteRemove(tag) + folder !== null -> onRemoteRemove(folder) + } + } + } + } + } + onSyncCompleted() + } + } + + private fun localDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, removed: Boolean = false) { + GlobalScope.launch { + val database = gDriveDatabase + if (database === null) { + return@launch + } + + val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() + existing.apply { + uuid = itemUUID + type = itemType.name + lastUpdateTimestamp = Math.max(gDriveUpdateTimestamp + 1, getTrueCurrentTime()) + localStateDeleted = removed + save(database) + } + } + } + + private fun remoteDatabaseUpdate(itemType: GDriveDataType, itemUUID: String) { + GlobalScope.launch { + val database = gDriveDatabase + if (database === null) { + return@launch + } + + val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() + existing.apply { + uuid = itemUUID + type = GDriveDataType.NOTE.name + lastUpdateTimestamp = gDriveUpdateTimestamp + localStateDeleted = gDriveStateDeleted + save(database) + } + } + } + override fun insert(note: INoteContainer) { if (!isValidController || note !is FirebaseNote) { return @@ -167,8 +342,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } IRemoteDatabaseUtils.onRemoteInsert(context, note) notifyImageIds(note) { - imageSync?.insert(it) + // imageSync?.insert(it) } + remoteDatabaseUpdate(GDriveDataType.NOTE, note.uuid) } override fun onRemoteRemove(note: INoteContainer) { @@ -181,6 +357,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } IRemoteDatabaseUtils.onRemoteRemove(context, note) + remoteDatabaseUpdate(GDriveDataType.NOTE, note.uuid) } override fun onRemoteInsert(tag: ITagContainer) { @@ -193,6 +370,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } IRemoteDatabaseUtils.onRemoteInsert(context, tag) + remoteDatabaseUpdate(GDriveDataType.TAG, tag.uuid) } override fun onRemoteRemove(tag: ITagContainer) { @@ -205,6 +383,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } IRemoteDatabaseUtils.onRemoteRemove(context, tag) + remoteDatabaseUpdate(GDriveDataType.TAG, tag.uuid) } override fun onRemoteInsert(folder: IFolderContainer) { @@ -217,6 +396,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } IRemoteDatabaseUtils.onRemoteInsert(context, folder) + remoteDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid) } override fun onRemoteRemove(folder: IFolderContainer) { @@ -229,6 +409,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } IRemoteDatabaseUtils.onRemoteRemove(context, folder) + remoteDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid) } fun notifyImageIds(note: INoteContainer, onImageUUID: (ImageUUID) -> Unit) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 466db393..8ef2fb15 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -1,16 +1,18 @@ package com.bijoysingh.quicknote.drive +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadData +import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.google.api.services.drive.model.File import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean -const val KEY_G_DRIVE_SYNC_LAST_SCAN = "drive_g_folder_sync_last_sync" -const val G_DRIVE_LAST_MODIFIED_ERROR_MARGIN = 7 * 1000 * 60 * 60 * 24L -const val G_DRIVE_LAST_MODIFIED_UPDATE_ERROR_MARGIN = 1000 * 60 * 60 * 1L - class GDriveRemoteFolder( + val dataType: GDriveDataType, + val database: GDriveUploadDataDao, val helper: GDriveServiceHelper, val uuidToObject: (String) -> T?) { @@ -33,6 +35,7 @@ class GDriveRemoteFolder( val localFileIds = emptyMap().toMutableMap() files.forEach { file -> localFileIds[file.name] = file.id + notifyDriveData(file) } contentFiles.clear() contentFiles.putAll(localFileIds) @@ -44,6 +47,40 @@ class GDriveRemoteFolder( } } + private fun notifyDriveData(file: File, deleted: Boolean = false) { + val modifiedTime = file.modifiedTime?.value ?: 0L + notifyDriveData(file.id, file.name, modifiedTime, deleted) + } + + private fun notifyDriveData(uid: String, name: String, modifiedTime: Long, deleted: Boolean = false) { + GlobalScope.launch { + val uploadData = database.getByUUID(dataType.name, name) + if (uploadData == null) { + GDriveUploadData().apply { + uuid = name + type = dataType.name + fileId = uid + gDriveUpdateTimestamp = modifiedTime + gDriveStateDeleted = deleted + save(database) + } + return@launch + } + + + if (uploadData.gDriveUpdateTimestamp != modifiedTime + || uploadData.fileId != uid + || uploadData.gDriveStateDeleted != deleted) { + uploadData.apply { + gDriveUpdateTimestamp = modifiedTime + fileId = uid + gDriveStateDeleted = deleted + save(database) + } + } + } + } + fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { GlobalScope.launch(Dispatchers.IO) { deletedLoading.set(true) @@ -53,6 +90,7 @@ class GDriveRemoteFolder( val localFileIds = emptyMap().toMutableMap() files.forEach { file -> localFileIds[file.name] = file.id + notifyDriveData(file, true) } deletedFiles.clear() deletedFiles.putAll(localFileIds) @@ -90,16 +128,22 @@ class GDriveRemoteFolder( try { val data = Gson().toJson(item) val fileId = contentFiles[uuid] + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() if (fileId !== null) { - helper.saveFile(fileId, uuid, data).addOnCompleteListener { - helper.updateLastModifiedTime(contentFolderUid) + helper.saveFile(fileId, uuid, data, timestamp).addOnCompleteListener { + val file = it.result + if (file !== null) { + notifyDriveData(file.id, uuid, timestamp) + } } return } - - helper.createFileWithData(contentFolderUid, uuid, data).addOnCompleteListener { - contentFiles[uuid] = it.result ?: INVALID_FILE_ID - helper.updateLastModifiedTime(contentFolderUid) + helper.createFileWithData(contentFolderUid, uuid, data, timestamp).addOnCompleteListener { + val file = it.result + if (file !== null) { + contentFiles[uuid] = file.id + notifyDriveData(file.id, uuid, timestamp) + } } } catch (exception: Exception) { } @@ -117,9 +161,13 @@ class GDriveRemoteFolder( contentFiles.remove(uuid) } - helper.createFileWithData(deletedFolderUid, uuid).addOnCompleteListener { - deletedFiles[uuid] = it.result ?: INVALID_FILE_ID - helper.updateLastModifiedTime(deletedFolderUid) + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() + helper.createFileWithData(deletedFolderUid, uuid, "", timestamp).addOnCompleteListener { + val file = it.result + if (file !== null) { + deletedFiles[uuid] = file.id + notifyDriveData(file.id, uuid, timestamp, true) + } } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 6938ba33..acc4fbff 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -64,9 +64,7 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) val finalFileName = "${id.noteUuid}::${id.imageUuid}" - helper.createFileWithData(contentFolderUid, finalFileName, imageFile).addOnCompleteListener { - helper.updateLastModifiedTime(contentFolderUid) - } + helper.createFileWithData(contentFolderUid, finalFileName, imageFile, getTrueCurrentTime()).addOnCompleteListener {} } fun delete(id: ImageUUID) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 2dd17a99..fcca2617 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -25,13 +25,6 @@ const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" -const val STORE_KEY_G_DRIVE_LAST_MODIFICATION_TIME = "store_key_g_drive_last_modification_time" -var sGDriveLastModificationTime: Long - get() = CoreConfig.instance.store().get(STORE_KEY_G_DRIVE_LAST_MODIFICATION_TIME, 0L) - set(value) = CoreConfig.instance.store().put(STORE_KEY_G_DRIVE_LAST_MODIFICATION_TIME, value) - -var updateCheckerFileId: String? = null - class ErrorCallable(val callable: Callable) : Callable { override fun call(): T { try { @@ -54,42 +47,37 @@ fun getTrueCurrentTime(): Long { class GDriveServiceHelper(private val mDriveService: Drive) { private val mExecutor = Executors.newFixedThreadPool(8) - private val mSerialExecutor = Executors.newSingleThreadExecutor() fun execute(callable: Callable): Task { return Tasks.call(mExecutor, ErrorCallable(callable)) } - fun createFileWithData(folderId: String, name: String, content: String = ""): Task { - val currentTime = getTrueCurrentTime() - val contentToSave = if (content.isEmpty()) currentTime.toString() else content + fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { + val contentToSave = if (content.isEmpty()) updateTime.toString() else content return execute(Callable { val metadata = File() .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) - .setModifiedTime(DateTime(currentTime)) + .setModifiedTime(DateTime(updateTime)) .setName(name) val contentStream = ByteArrayContent.fromString("text/plain", contentToSave) - val googleFile = mDriveService.files().create(metadata, contentStream).execute() - googleFile?.id ?: INVALID_FILE_ID + mDriveService.files().create(metadata, contentStream).execute() }) } - fun createFileWithData(folderId: String, name: String, file: java.io.File): Task { - val currentTime = getTrueCurrentTime() - return execute(Callable { + fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { + return execute(Callable { val metadata = File() .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) - .setModifiedTime(DateTime(currentTime)) + .setModifiedTime(DateTime(updateTime)) .setName(name) val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) - val googleFile = mDriveService.files().create(metadata, mediaContent).execute() - googleFile?.id ?: INVALID_FILE_ID + mDriveService.files().create(metadata, mediaContent).execute() }) } - fun createFolder(parentUid: String, folderName: String): Task { + fun createFolder(parentUid: String, folderName: String): Task { return execute(Callable { val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) @@ -98,12 +86,10 @@ class GDriveServiceHelper(private val mDriveService: Drive) { if (!parentUid.isEmpty()) { metadata.parents = listOf(parentUid) } - val googleFile = mDriveService.files().create(metadata).execute() - googleFile?.id ?: INVALID_FILE_ID + mDriveService.files().create(metadata).execute() }) } - fun readFile(fileId: String): Task { return execute(Callable { mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> @@ -130,12 +116,11 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun saveFile(fileId: String, name: String, content: String): Task { - return execute(Callable { - val metadata = File().setName(name) + fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { + return execute(Callable { + val metadata = File().setModifiedTime(DateTime(updateTime)).setName(name) val contentStream = ByteArrayContent.fromString("text/plain", content) mDriveService.files().update(fileId, metadata, contentStream).execute() - null }) } @@ -144,6 +129,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { mDriveService.files().list() .setSpaces("drive") .setPageSize(1000) + .setFields("files(name, id, modifiedTime, mimeType)") .setQ("mimeType = '$mimeType' and '$parentUid' in parents") .execute() }) @@ -164,7 +150,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun getSubRootFolders(parentUid: String, names: List): Task { var nameQueryBuilder = "name = '${names[0]}'" - names.subList(1, names.lastIndex).forEach { + names.subList(1, names.lastIndex + 1).forEach { nameQueryBuilder += " or name = '$it'" } return execute(Callable { @@ -191,29 +177,8 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } createFolder(parentUid, name).addOnCompleteListener { createTask -> - onFolderId(createTask.result) + onFolderId(createTask.result?.id ?: INVALID_FILE_ID) } } } - - fun getLastUpdateTime(): Task { - val folderId = updateCheckerFileId - return Tasks.call(mSerialExecutor, ErrorCallable(Callable { - when { - (folderId === null) -> throw RuntimeException("Folder not set") - else -> mDriveService.files().get(folderId).execute() - } - })) - } - - fun updateLastModifiedTime(folderUid: String): Long { - val currentTime = getTrueCurrentTime() - Tasks.call(mSerialExecutor, ErrorCallable(Callable { - val metadata = File() - .setModifiedTime(DateTime(currentTime)) - .setModifiedByMeTime(DateTime(currentTime)) - mDriveService.files().update(folderUid, metadata).execute() - })) - return currentTime - } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index f34e328d..24193d0f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -68,8 +68,8 @@ class ScarletAuthenticator() : IAuthenticator { } override fun openLoginActivity(context: Context) = Runnable { - // context.startActivity(Intent(context, GDriveLoginActivity::class.java)) - context.startActivity(Intent(context, LoginActivity::class.java)) + context.startActivity(Intent(context, GDriveLoginActivity::class.java)) + // context.startActivity(Intent(context, LoginActivity::class.java)) } override fun openForgetMeActivity(context: Context) = Runnable { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index e4709ab8..f6716c6e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -3,6 +3,7 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context import android.support.v7.app.AppCompatActivity import com.bijoysingh.quicknote.BuildConfig +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.openIfNeeded import com.bijoysingh.quicknote.firebase.support.RemoteConfigFetcher import com.bijoysingh.quicknote.firebase.support.ScarletAuthenticator @@ -38,4 +39,12 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { override fun startListener(activity: AppCompatActivity) { openIfNeeded(activity) } + + override fun resyncDrive(onSyncCompleted: () -> Unit) { + if (gDrive === null) { + onSyncCompleted() + return + } + gDrive?.resync(onSyncCompleted) + } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 89731a2a..389ae43a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -11,12 +11,12 @@ class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { override fun onlineSave() { super.onlineSave() firebase?.insert(folder.getFirebaseFolder()) - gDrive?.insert(folder.getFirebaseFolder()) + gDrive?.notifyInsert(folder.getFirebaseFolder()) } override fun delete() { super.delete() firebase?.remove(folder.getFirebaseFolder()) - gDrive?.remove(folder.getFirebaseFolder()) + gDrive?.notifyRemove(folder.getFirebaseFolder()) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index abc334e0..2c09c69b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -12,12 +12,12 @@ class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { override fun onlineSave(context: Context) { super.onlineSave(context) firebase?.insert(note.getFirebaseNote()) - gDrive?.insert(note.getFirebaseNote()) + gDrive?.notifyInsert(note.getFirebaseNote()) } override fun onlineDelete(context: Context) { super.onlineDelete(context) firebase?.remove(note.getFirebaseNote()) - gDrive?.remove(note.getFirebaseNote()) + gDrive?.notifyRemove(note.getFirebaseNote()) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index e890beb9..9e3bc8a2 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -11,12 +11,12 @@ class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { override fun onlineSave() { super.onlineSave() firebase?.insert(tag.getFirebaseTag()) - gDrive?.insert(tag.getFirebaseTag()) + gDrive?.notifyInsert(tag.getFirebaseTag()) } override fun delete() { super.delete() firebase?.remove(tag.getFirebaseTag()) - gDrive?.insert(tag.getFirebaseTag()) + gDrive?.notifyRemove(tag.getFirebaseTag()) } } \ No newline at end of file From 4051caacfb7cffd6649ac4dace8bd342d31d974e Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 3 May 2019 23:53:40 +0100 Subject: [PATCH 010/134] [GDrive] Adding onRemoteInsert support --- .../database/remote/IRemoteDatabaseUtils.kt | 20 +- .../quicknote/database/GDriveUploadData.kt | 6 +- .../quicknote/drive/GDriveLoginActivity.kt | 4 + .../quicknote/drive/GDriveRemoteDatabase.kt | 355 ++++++++++++------ .../quicknote/drive/GDriveRemoteFolder.kt | 42 +-- .../quicknote/drive/GDriveRemoteFolderBase.kt | 48 +++ .../drive/GDriveRemoteImageFolder.kt | 62 ++- .../quicknote/drive/GDriveServiceHelper.kt | 1 - 8 files changed, 378 insertions(+), 160 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt b/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt index c1c66c74..1f173206 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt @@ -90,8 +90,20 @@ object IRemoteDatabaseUtils { } } + fun onRemoteRemoveNote(context: Context, noteUUID: String) { + val existingNote = CoreConfig.notesDb.getByUUID(noteUUID) + if (existingNote !== null && !existingNote.disableBackup) { + existingNote.deleteWithoutSync(context) + sendNoteBroadcast(context, NoteBroadcast.NOTE_DELETED, existingNote.uuid) + } + } + fun onRemoteRemove(context: Context, tag: ITagContainer) { - val existingTag = CoreConfig.tagsDb.getByUUID(tag.uuid()) + onRemoteRemoveTag(context, tag.uuid()) + } + + fun onRemoteRemoveTag(context: Context, tagUUID: String) { + val existingTag = CoreConfig.tagsDb.getByUUID(tagUUID) if (existingTag !== null) { existingTag.deleteWithoutSync() sendNoteBroadcast(context, NoteBroadcast.TAG_DELETED, existingTag.uuid) @@ -99,7 +111,11 @@ object IRemoteDatabaseUtils { } fun onRemoteRemove(context: Context, folder: IFolderContainer) { - val existingFolder = CoreConfig.foldersDb.getByUUID(folder.uuid()) + onRemoteRemoveFolder(context, folder.uuid()) + } + + fun onRemoteRemoveFolder(context: Context, folderUUID: String) { + val existingFolder = CoreConfig.foldersDb.getByUUID(folderUUID) if (existingFolder !== null) { existingFolder.deleteWithoutSync() CoreConfig.notesDb.getAll().filter { it.folder == existingFolder.uuid }.forEach { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index 3329a141..d3e8bb0e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -5,7 +5,8 @@ import android.arch.persistence.room.* enum class GDriveDataType { NOTE, TAG, - FOLDER + FOLDER, + IMAGE, } @Entity(tableName = "gdrive_upload", indices = [Index("uuid")]) @@ -50,4 +51,7 @@ interface GDriveUploadDataDao { @Query("SELECT * FROM gdrive_upload WHERE uuid = :uuid AND type = :type LIMIT 1") fun getByUUID(type: String, uuid: String): GDriveUploadData? + + @Query("SELECT * FROM gdrive_upload WHERE type = :type") + fun getByType(type: String): List } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 986298bb..e04a7caa 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" var sGDriveFirstSyncNote: Boolean get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) @@ -49,6 +50,9 @@ var sGDriveFirstSyncTag: Boolean var sGDriveFirstSyncFolder: Boolean get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) +var sGDriveFirstSyncImage: Boolean + get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) + set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index ba340f0a..2b7a1cd9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,19 +1,19 @@ package com.bijoysingh.quicknote.drive import android.content.Context -import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.bijoysingh.quicknote.database.genGDriveUploadDatabase import com.bijoysingh.quicknote.firebase.data.* +import com.google.gson.Gson +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.IFolderContainer import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.INoteContainer import com.maubis.scarlet.base.core.tag.ITagContainer -import com.maubis.scarlet.base.database.remote.IRemoteDatabase import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -27,7 +27,7 @@ const val FOLDER_NAME_DELETED_NOTES = "deleted_notes" const val FOLDER_NAME_DELETED_TAGS = "deleted_tags" const val FOLDER_NAME_DELETED_FOLDERS = "deleted_folders" -class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDatabase { +class GDriveRemoteDatabase(val weakContext: WeakReference) { var gDriveDatabase: GDriveUploadDataDao? = null @@ -39,8 +39,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat private var tagsSync: GDriveRemoteFolder? = null private var imageSync: GDriveRemoteImageFolder? = null - override fun init(userId: String) {} - fun init(helper: GDriveServiceHelper) { val context = weakContext.get() if (context === null) { @@ -60,7 +58,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat foldersSync = GDriveRemoteFolder(GDriveDataType.FOLDER, gDriveDatabase!!, helper) { CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() } - imageSync = GDriveRemoteImageFolder(helper) + imageSync = GDriveRemoteImageFolder(GDriveDataType.IMAGE, gDriveDatabase!!, helper) GlobalScope.launch { driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { @@ -76,32 +74,31 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat when (folderName) { FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNote) { - CoreConfig.instance.notesDatabase().getAll().forEach { - gDrive?.notifyInsert(it.getFirebaseNote()) - } + GlobalScope.launch { resyncNotesSync { } } sGDriveFirstSyncNote = true } } FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncTag) { - CoreConfig.instance.tagsDatabase().getAll().forEach { - gDrive?.notifyInsert(it.getFirebaseTag()) - } + GlobalScope.launch { resyncTagsSync { } } sGDriveFirstSyncTag = true } } FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncFolder) { - CoreConfig.instance.foldersDatabase().getAll().forEach { - gDrive?.notifyInsert(it.getFirebaseFolder()) - } + GlobalScope.launch { resyncFoldersSync { } } sGDriveFirstSyncFolder = true } } + FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { + if (!sGDriveFirstSyncImage) { + GlobalScope.launch { resyncImagessSync { } } + sGDriveFirstSyncImage = true + } + } FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) {} FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) {} FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) {} - FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) {} } } @@ -130,7 +127,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } } - override fun reset() { + fun reset() { isValidController = false driveHelper = null notesSync = null @@ -139,11 +136,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat imageSync = null } - override fun logout() { - reset() - } - - override fun deleteEverything() { + private fun deleteEverything() { if (!isValidController) { return } @@ -154,6 +147,43 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat return } localDatabaseUpdate(GDriveDataType.NOTE, note.uuid) + + val database = gDriveDatabase + if (database === null) { + return + } + + GlobalScope.launch { + val imageUUIDs = HashSet() + notifyImageIds(note) { imageUUIDs.add(it) } + + database.getByType(GDriveDataType.IMAGE.name) + .filter { + val uuid = toImageUUID(it.uuid) + uuid?.noteUuid == note.uuid && !imageUUIDs.contains(uuid) + }.forEach { + it.apply { + lastUpdateTimestamp = getTrueCurrentTime() + localStateDeleted = true + save(database) + } + } + + imageUUIDs.forEach { + val existing = database.getByUUID(GDriveDataType.IMAGE.name, it.name()) + if (existing !== null) { + return@launch + } + + GDriveUploadData().apply { + uuid = it.name() + type = GDriveDataType.IMAGE.name + lastUpdateTimestamp = getTrueCurrentTime() + localStateDeleted = false + save(database) + } + } + } } fun notifyInsert(tag: ITagContainer) { @@ -191,58 +221,93 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat localDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid, true) } - fun resync(onSyncCompleted: () -> Unit) { - if (!isValidController) { - onSyncCompleted() - return + fun resyncNotesSync(onSyncCompleted: () -> Unit) { + gDriveDatabase?.getByType(GDriveDataType.NOTE.name)?.forEach { + val sameDelete = it.localStateDeleted == it.gDriveStateDeleted + val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp + if (!sameUpdateTime) { + when { + sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> + CoreConfig.instance.notesDatabase().getByUUID(it.uuid)?.getFirebaseNote()?.apply { + insert(this) + } + sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertNote(it) + !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeNote(it.uuid) + !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveNote(it) + } + } } + onSyncCompleted() + } - GlobalScope.launch { - gDriveDatabase?.all?.forEach { - val sameDelete = it.localStateDeleted == it.gDriveStateDeleted - val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp - if (!sameUpdateTime) { - var note: FirebaseNote? = null - var tag: FirebaseTag? = null - var folder: FirebaseFolder? = null - when (it.type) { - GDriveDataType.NOTE.name -> note = CoreConfig.instance.notesDatabase().getByUUID(it.uuid)?.getFirebaseNote() - GDriveDataType.TAG.name -> tag = CoreConfig.instance.tagsDatabase().getByUUID(it.uuid)?.getFirebaseTag() - GDriveDataType.FOLDER.name -> folder = CoreConfig.instance.foldersDatabase().getByUUID(it.uuid)?.getFirebaseFolder() - } - - when { - sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> { - when { - note !== null -> insert(note) - tag !== null -> insert(tag) - folder !== null -> insert(folder) - } - } - sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> { - when { - note !== null -> onRemoteInsert(note) - tag !== null -> onRemoteInsert(tag) - folder !== null -> onRemoteInsert(folder) - } + fun resyncTagsSync(onSyncCompleted: () -> Unit) { + gDriveDatabase?.getByType(GDriveDataType.TAG.name)?.forEach { + val sameDelete = it.localStateDeleted == it.gDriveStateDeleted + val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp + if (!sameUpdateTime) { + when { + sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> + CoreConfig.instance.tagsDatabase().getByUUID(it.uuid)?.getFirebaseTag()?.apply { + insert(this) } - !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> { - when { - note !== null -> remove(note) - tag !== null -> remove(tag) - folder !== null -> remove(folder) - } + sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertTag(it) + !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeTag(it.uuid) + !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveTag(it) + } + } + } + onSyncCompleted() + } + + fun resyncFoldersSync(onSyncCompleted: () -> Unit) { + gDriveDatabase?.getByType(GDriveDataType.FOLDER.name)?.forEach { + val sameDelete = it.localStateDeleted == it.gDriveStateDeleted + val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp + if (!sameUpdateTime) { + when { + sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> + CoreConfig.instance.foldersDatabase().getByUUID(it.uuid)?.getFirebaseFolder()?.apply { + insert(this) } - !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> { - when { - note !== null -> onRemoteRemove(note) - tag !== null -> onRemoteRemove(tag) - folder !== null -> onRemoteRemove(folder) - } + sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertFolder(it) + !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeFolder(it.uuid) + !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveFolder(it) + } + } + } + onSyncCompleted() + } + + fun resyncImagessSync(onSyncCompleted: () -> Unit) { + gDriveDatabase?.getByType(GDriveDataType.IMAGE.name)?.forEach { + val sameDelete = it.localStateDeleted == it.gDriveStateDeleted + val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp + if (!sameUpdateTime) { + when { + sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> + toImageUUID(it.uuid)?.apply { + insert(this) } - } + sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertImage(it) + !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeImage(it.uuid) + !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveImage(it) } } + } + onSyncCompleted() + } + + fun resync(onSyncCompleted: () -> Unit) { + if (!isValidController) { + onSyncCompleted() + return + } + + GlobalScope.launch { + resyncNotesSync {} + resyncTagsSync {} + resyncFoldersSync {} + resyncImagessSync {} onSyncCompleted() } } @@ -283,56 +348,67 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat } } - override fun insert(note: INoteContainer) { + private fun insert(note: INoteContainer) { if (!isValidController || note !is FirebaseNote) { return } notesSync?.insert(note.uuid, note) - notifyImageIds(note) { - imageSync?.insert(it) + } + + private fun insert(imageUUID: ImageUUID) { + if (!isValidController) { + return } + imageSync?.insert(imageUUID) } - override fun insert(tag: ITagContainer) { + private fun insert(tag: ITagContainer) { if (!isValidController || tag !is FirebaseTag) { return } tagsSync?.insert(tag.uuid, tag) } - override fun insert(folder: IFolderContainer) { + private fun insert(folder: IFolderContainer) { if (!isValidController || folder !is FirebaseFolder) { return } foldersSync?.insert(folder.uuid, folder) } - override fun remove(note: INoteContainer) { - if (!isValidController || note !is FirebaseNote) { + private fun removeNote(uuid: String) { + if (!isValidController) { return } - notesSync?.delete(note.uuid) - notifyImageIds(note) { - imageSync?.delete(it) + notesSync?.delete(uuid) + } + + private fun removeTag(uuid: String) { + if (!isValidController) { + return } + tagsSync?.delete(uuid) } - override fun remove(tag: ITagContainer) { - if (!isValidController || tag !is FirebaseTag) { + private fun removeFolder(uuid: String) { + if (!isValidController) { return } - tagsSync?.delete(tag.uuid) + foldersSync?.delete(uuid) } - override fun remove(folder: IFolderContainer) { - if (!isValidController || folder !is FirebaseFolder) { + private fun removeImage(uuid: String) { + if (!isValidController) { return } - foldersSync?.delete(folder.uuid) + val imageUUID = toImageUUID(uuid) + if (imageUUID !== null) { + imageSync?.delete(imageUUID) + } } - override fun onRemoteInsert(note: INoteContainer) { - if (!isValidController || note !is FirebaseNote) { + private fun onRemoteInsertNote(data: GDriveUploadData) { + if (!isValidController) { return } @@ -340,15 +416,19 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat if (context === null) { return } - IRemoteDatabaseUtils.onRemoteInsert(context, note) - notifyImageIds(note) { - // imageSync?.insert(it) + + onRemoteInsert(data.fileId) { + try { + val item = Gson().fromJson(it, FirebaseNote::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseUpdate(GDriveDataType.NOTE, item.uuid) + } catch (exception: Exception) { + } } - remoteDatabaseUpdate(GDriveDataType.NOTE, note.uuid) } - override fun onRemoteRemove(note: INoteContainer) { - if (!isValidController || note !is FirebaseNote) { + private fun onRemoteInsertTag(data: GDriveUploadData) { + if (!isValidController) { return } @@ -356,12 +436,19 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat if (context === null) { return } - IRemoteDatabaseUtils.onRemoteRemove(context, note) - remoteDatabaseUpdate(GDriveDataType.NOTE, note.uuid) + + onRemoteInsert(data.fileId) { + try { + val item = Gson().fromJson(it, FirebaseTag::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) + } catch (exception: Exception) { + } + } } - override fun onRemoteInsert(tag: ITagContainer) { - if (!isValidController || tag !is FirebaseTag) { + private fun onRemoteInsertFolder(data: GDriveUploadData) { + if (!isValidController) { return } @@ -369,12 +456,19 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat if (context === null) { return } - IRemoteDatabaseUtils.onRemoteInsert(context, tag) - remoteDatabaseUpdate(GDriveDataType.TAG, tag.uuid) + + onRemoteInsert(data.fileId) { + try { + val item = Gson().fromJson(it, FirebaseFolder::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + } catch (exception: Exception) { + } + } } - override fun onRemoteRemove(tag: ITagContainer) { - if (!isValidController || tag !is FirebaseTag) { + private fun onRemoteInsertImage(data: GDriveUploadData) { + if (!isValidController) { return } @@ -382,12 +476,18 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat if (context === null) { return } - IRemoteDatabaseUtils.onRemoteRemove(context, tag) - remoteDatabaseUpdate(GDriveDataType.TAG, tag.uuid) + + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null) { + val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { + remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + } + } } - override fun onRemoteInsert(folder: IFolderContainer) { - if (!isValidController || folder !is FirebaseFolder) { + private fun onRemoteRemoveNote(data: GDriveUploadData) { + if (!isValidController) { return } @@ -395,12 +495,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat if (context === null) { return } - IRemoteDatabaseUtils.onRemoteInsert(context, folder) - remoteDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid) + IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) + remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) } - override fun onRemoteRemove(folder: IFolderContainer) { - if (!isValidController || folder !is FirebaseFolder) { + private fun onRemoteRemoveTag(data: GDriveUploadData) { + if (!isValidController) { return } @@ -408,11 +508,46 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) : IRemoteDat if (context === null) { return } - IRemoteDatabaseUtils.onRemoteRemove(context, folder) - remoteDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid) + IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) + remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) + } + + private fun onRemoteRemoveFolder(data: GDriveUploadData) { + if (!isValidController) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) + remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + } + + private fun onRemoteRemoveImage(data: GDriveUploadData) { + if (!isValidController) { + return + } + + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null) { + val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + imageFile.delete() + remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + } + } + + private fun onRemoteInsert(fileId: String, onDataAvailable: (String) -> Unit) { + driveHelper?.readFile(fileId)?.addOnCompleteListener { + val data = it.result + if (data !== null) { + onDataAvailable(data) + } + } } - fun notifyImageIds(note: INoteContainer, onImageUUID: (ImageUUID) -> Unit) { + private fun notifyImageIds(note: INoteContainer, onImageUUID: (ImageUUID) -> Unit) { val imageIds = FormatBuilder() .getFormats(note.description()) .filter { it.formatType == FormatType.IMAGE } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 8ef2fb15..adbee400 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -11,10 +11,10 @@ import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean class GDriveRemoteFolder( - val dataType: GDriveDataType, - val database: GDriveUploadDataDao, - val helper: GDriveServiceHelper, - val uuidToObject: (String) -> T?) { + dataType: GDriveDataType, + database: GDriveUploadDataDao, + helper: GDriveServiceHelper, + val uuidToObject: (String) -> T?): GDriveRemoteFolderBase(dataType, database, helper) { var contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -47,40 +47,6 @@ class GDriveRemoteFolder( } } - private fun notifyDriveData(file: File, deleted: Boolean = false) { - val modifiedTime = file.modifiedTime?.value ?: 0L - notifyDriveData(file.id, file.name, modifiedTime, deleted) - } - - private fun notifyDriveData(uid: String, name: String, modifiedTime: Long, deleted: Boolean = false) { - GlobalScope.launch { - val uploadData = database.getByUUID(dataType.name, name) - if (uploadData == null) { - GDriveUploadData().apply { - uuid = name - type = dataType.name - fileId = uid - gDriveUpdateTimestamp = modifiedTime - gDriveStateDeleted = deleted - save(database) - } - return@launch - } - - - if (uploadData.gDriveUpdateTimestamp != modifiedTime - || uploadData.fileId != uid - || uploadData.gDriveStateDeleted != deleted) { - uploadData.apply { - gDriveUpdateTimestamp = modifiedTime - fileId = uid - gDriveStateDeleted = deleted - save(database) - } - } - } - } - fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { GlobalScope.launch(Dispatchers.IO) { deletedLoading.set(true) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt new file mode 100644 index 00000000..fd5fefba --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -0,0 +1,48 @@ +package com.bijoysingh.quicknote.drive + +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadData +import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.google.api.services.drive.model.File +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +abstract class GDriveRemoteFolderBase( + val dataType: GDriveDataType, + val database: GDriveUploadDataDao, + val helper: GDriveServiceHelper) { + + protected fun notifyDriveData(file: File, deleted: Boolean = false) { + val modifiedTime = file.modifiedTime?.value ?: 0L + notifyDriveData(file.id, file.name, modifiedTime, deleted) + } + + protected fun notifyDriveData(uid: String, name: String, modifiedTime: Long, deleted: Boolean = false) { + GlobalScope.launch { + val uploadData = database.getByUUID(dataType.name, name) + if (uploadData == null) { + GDriveUploadData().apply { + uuid = name + type = dataType.name + fileId = uid + gDriveUpdateTimestamp = modifiedTime + gDriveStateDeleted = deleted + save(database) + } + return@launch + } + + + if (uploadData.gDriveUpdateTimestamp != modifiedTime + || uploadData.fileId != uid + || uploadData.gDriveStateDeleted != deleted) { + uploadData.apply { + gDriveUpdateTimestamp = modifiedTime + fileId = uid + gDriveStateDeleted = deleted + save(database) + } + } + } + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index acc4fbff..1ac1afb6 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -1,5 +1,7 @@ package com.bijoysingh.quicknote.drive +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -10,6 +12,9 @@ import java.util.concurrent.atomic.AtomicBoolean data class ImageUUID( val noteUuid: String, val imageUuid: String) { + + fun name(): String = "$noteUuid::$imageUuid" + override fun equals(other: Any?): Boolean { if (other !is ImageUUID) { return false @@ -22,7 +27,20 @@ data class ImageUUID( } } -class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { +fun toImageUUID(imageUuid: String): ImageUUID? { + val components = imageUuid.split("::") + if (components.size == 2) { + val noteUuid = components[0] + val imageId = components[1] + return ImageUUID(noteUuid, imageId) + } + return null +} + +class GDriveRemoteImageFolder( + dataType: GDriveDataType, + database: GDriveUploadDataDao, + helper: GDriveServiceHelper) : GDriveRemoteFolderBase(dataType, database, helper) { val contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -38,11 +56,10 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { val imageFiles = it.result?.files if (imageFiles !== null) { imageFiles.forEach { imageFile -> - val components = imageFile.name.split("::") - if (components.size == 2) { - val noteUuid = components[0] - val imageId = components[1] - contentFiles[ImageUUID(noteUuid, imageId)] = imageFile.id + val components = toImageUUID(imageFile.name) + if (components !== null) { + contentFiles[components] = imageFile.id + notifyDriveData(imageFile) } } contentLoading.set(false) @@ -52,6 +69,28 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { } } + fun onRemoteInsert(id: ImageUUID) { + if (contentLoading.get()) { + contentPendingActions.add(id) + return + } + + if (contentFiles.containsKey(id)) { + return + } + + val gDriveUUID = id.name() + val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() + val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) + helper.createFileWithData(contentFolderUid, gDriveUUID, imageFile, timestamp).addOnCompleteListener { + val file = it.result + if (file !== null) { + contentFiles[id] = file.id + notifyDriveData(file.id, gDriveUUID, timestamp) + } + } + } + fun insert(id: ImageUUID) { if (contentLoading.get()) { contentPendingActions.add(id) @@ -62,9 +101,16 @@ class GDriveRemoteImageFolder(val helper: GDriveServiceHelper) { return } + val gDriveUUID = id.name() + val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) - val finalFileName = "${id.noteUuid}::${id.imageUuid}" - helper.createFileWithData(contentFolderUid, finalFileName, imageFile, getTrueCurrentTime()).addOnCompleteListener {} + helper.createFileWithData(contentFolderUid, gDriveUUID, imageFile, timestamp).addOnCompleteListener { + val file = it.result + if (file !== null) { + contentFiles[id] = file.id + notifyDriveData(file.id, gDriveUUID, timestamp) + } + } } fun delete(id: ImageUUID) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index fcca2617..d74b74a1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -9,7 +9,6 @@ import com.google.api.client.util.DateTime import com.google.api.services.drive.Drive import com.google.api.services.drive.model.File import com.google.api.services.drive.model.FileList -import com.maubis.scarlet.base.config.CoreConfig import java.io.BufferedReader import java.io.FileOutputStream import java.io.InputStreamReader From f307e5344a4726d0da6b2ead8a2321d6b589b5e0 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 7 May 2019 23:13:59 +0100 Subject: [PATCH 011/134] [GDrive] Updating activity --- app/build.gradle | 2 +- base/build.gradle | 8 +- .../com/maubis/scarlet/base/MainActivity.kt | 6 +- .../maubis/scarlet/base/config/CoreConfig.kt | 6 +- .../scarlet/base/config/MaterialNoteConfig.kt | 2 +- .../base/support/sheets/LithoBottomSheet.kt | 19 ++- base/src/main/res/drawable/gdrive_icon.png | Bin 0 -> 29663 bytes base/src/main/res/font/monserrat_bold.xml | 7 + base/src/main/res/font/monserrat_medium.xml | 7 + .../main/res/font/montserrat_extrabold.ttf | Bin 0 -> 262108 bytes base/src/main/res/font/montserrat_medium.ttf | Bin 0 -> 260236 bytes base/src/main/res/values/colors.xml | 2 + base/src/main/res/values/strings.xml | 10 ++ scarlet/build.gradle | 4 + .../quicknote/drive/GDriveActivitySpecs.kt | 122 ++++++++++++++++++ .../quicknote/drive/GDriveLoginActivity.kt | 49 ++++--- .../quicknote/drive/GDriveRemoteDatabase.kt | 8 +- .../quicknote/scarlet/ScarletConfig.kt | 4 +- scarlet/src/main/res/layout/gdrive_login.xml | 69 ---------- scarlet/src/main/res/values/strings.xml | 6 - 20 files changed, 214 insertions(+), 117 deletions(-) create mode 100644 base/src/main/res/drawable/gdrive_icon.png create mode 100644 base/src/main/res/font/monserrat_bold.xml create mode 100644 base/src/main/res/font/monserrat_medium.xml create mode 100644 base/src/main/res/font/montserrat_extrabold.ttf create mode 100644 base/src/main/res/font/montserrat_medium.ttf create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt delete mode 100644 scarlet/src/main/res/layout/gdrive_login.xml diff --git a/app/build.gradle b/app/build.gradle index 54230293..a3add217 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { defaultConfig { applicationId "com.bijoysingh.quicknote" - minSdkVersion 17 + minSdkVersion 21 targetSdkVersion 28 versionCode 125 versionName '6.9.7' diff --git a/base/build.gradle b/base/build.gradle index 411a1250..8811c99d 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -66,13 +66,13 @@ dependencies { api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" def litho_version = "0.21.0" - implementation "com.facebook.litho:litho-core:$litho_version" - implementation "com.facebook.litho:litho-widget:$litho_version" - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + api "com.facebook.litho:litho-core:$litho_version" + api "com.facebook.litho:litho-widget:$litho_version" + api 'com.android.support.constraint:constraint-layout:1.1.3' compileOnly "com.facebook.litho:litho-annotations:$litho_version" kapt "com.facebook.litho:litho-processor:$litho_version" - implementation 'com.facebook.soloader:soloader:0.5.1' + api 'com.facebook.soloader:soloader:0.5.1' androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index fe5cb1b3..787d0772 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -14,16 +14,13 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteState -import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.support.NoteExporter import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.main.HomeNavigationState import com.maubis.scarlet.base.main.recycler.* -import com.maubis.scarlet.base.main.sheets.AlertBottomSheet import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteTrashSheet import com.maubis.scarlet.base.main.specs.MainActivityBottomBar @@ -43,7 +40,6 @@ import com.maubis.scarlet.base.service.getNoteIntentFilter import com.maubis.scarlet.base.settings.sheet.STORE_KEY_LINE_COUNT import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_ENABLED import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_HOME_ENABLED -import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount import com.maubis.scarlet.base.support.SearchConfig @@ -331,7 +327,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { topSyncingLayout.visibility = View.VISIBLE GlobalScope.launch { - CoreConfig.instance.resyncDrive { + CoreConfig.instance.resyncDrive(false) { GlobalScope.launch(Dispatchers.Main) { topSyncingLayout.visibility = View.GONE } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 4391965f..afdf710a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -31,6 +31,8 @@ abstract class CoreConfig(context: Context) { config.spanConfig.headingTypeface = ResourcesCompat.getFont(context, R.font.monserrat) ?: Typeface.DEFAULT FONT_MONSERRAT = config.spanConfig.headingTypeface + FONT_MONSERRAT_MEDIUM = ResourcesCompat.getFont(context, R.font.monserrat_medium) ?: Typeface.DEFAULT + FONT_MONSERRAT_BOLD = ResourcesCompat.getFont(context, R.font.monserrat_bold) ?: Typeface.DEFAULT FONT_OPEN_SANS = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT } @@ -62,7 +64,7 @@ abstract class CoreConfig(context: Context) { abstract fun imageCache(): ImageCache - abstract fun resyncDrive(onSyncCompleted: () -> Unit) + abstract fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) companion object { lateinit var instance: CoreConfig @@ -71,6 +73,8 @@ abstract class CoreConfig(context: Context) { val foldersDb get() = instance.foldersDatabase() var FONT_MONSERRAT: Typeface = Typeface.DEFAULT + var FONT_MONSERRAT_MEDIUM: Typeface = Typeface.DEFAULT + var FONT_MONSERRAT_BOLD: Typeface = Typeface.DEFAULT var FONT_OPEN_SANS: Typeface = Typeface.DEFAULT } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 95e28d21..1208ad8d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -67,5 +67,5 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun imageCache(): ImageCache = imageCache - override fun resyncDrive(onSyncCompleted: () -> Unit) = onSyncCompleted() + override fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) = onSyncCompleted() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 2b1990bc..1b2341aa 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -4,10 +4,10 @@ import android.app.Dialog import android.content.Context import android.graphics.Color import android.graphics.Typeface +import android.os.Bundle import android.support.design.widget.BottomSheetDialogFragment import android.support.v7.app.AppCompatActivity import android.view.View -import android.widget.ScrollView import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext @@ -20,6 +20,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.support.ui.BottomSheetTabletDialog import com.maubis.scarlet.base.support.ui.ThemeColorType fun openSheet(activity: AppCompatActivity, sheet: LithoBottomSheet) { @@ -48,6 +49,22 @@ fun getLithoBottomSheetButton(context: ComponentContext): Text.Builder { } abstract class LithoBottomSheet : BottomSheetDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val ctxt = context ?: activity + if (ctxt === null) { + return super.onCreateDialog(savedInstanceState) + } + + val isTablet = ctxt.resources.getBoolean(R.bool.is_tablet) + val dialog = when { + isTablet -> BottomSheetTabletDialog(ctxt, theme) + else -> super.onCreateDialog(savedInstanceState) + } + retainInstance = true + return dialog + } + override fun setupDialog(dialog: Dialog, style: Int) { val localContext = context if (localContext === null) { diff --git a/base/src/main/res/drawable/gdrive_icon.png b/base/src/main/res/drawable/gdrive_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ea1e50060aeca22802142a84bca930925d6c29 GIT binary patch literal 29663 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 literal 0 HcmV?d00001 diff --git a/base/src/main/res/font/monserrat_bold.xml b/base/src/main/res/font/monserrat_bold.xml new file mode 100644 index 00000000..4c3dbb84 --- /dev/null +++ b/base/src/main/res/font/monserrat_bold.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/font/monserrat_medium.xml b/base/src/main/res/font/monserrat_medium.xml new file mode 100644 index 00000000..151838c5 --- /dev/null +++ b/base/src/main/res/font/monserrat_medium.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/font/montserrat_extrabold.ttf b/base/src/main/res/font/montserrat_extrabold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6725d537d430c0c915644af44a3abeaf164b0433 GIT binary patch literal 262108 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

&yo7o^d0IGc!PMt~DGWVV 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

?`{lJ>rSbGf-3EytO-7Klc#M9WF$DOlECImujQ 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�}y&6uAD_Y$6NhT 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

L3^|io} zK2!#b(7M*hfRT?KY-l*x*08%dc)Q25BcyquKCdG?yCW~JGb^hzPuABTYHvT(P`b7D z4LRaJYNj1tM|Pjn(U+aw=WzBRAe6=g5k9gO>um!@1@;l5@Yc$I3D6;;Dd`RPK3t~$ z{1>x7{Y8raL@H+?Yx{2r-8*}Ex$E%U)wQ+f&YUEnaNm7jjOeKx9{Xd#_qKvQ{tP{U z=))$pxmwIj4o2({GG2HLpjM+jj%*A>mH+ zS^#AkImJ+131!i9&y3>oZ;2FRA zDCEHyG42%Jy4^21Ak|DS~Ijn2~8fp(}Q99d2>L|G-mX9Cq+j9b6_% zU*M2GH)@Zm&NcSL=hnnn$MQ_W)t{41pRXG+dGr5GesHY1^tIQ@Tw{CtyX8kKDjsEB zgKYS7pW}Ro_~d_3gv~}3-kt2GaC0Vx~`rQt+ z&7?`h9I14d$=CC_GAy0pI6}I})lqBEZnYga0%f*Q4k40RfLjC8qr4JQ{iJc|c0AC> zuI4a;9iiCqel;FznlEasX^a&?Tte3QPy}EJ?6=zohVwRCFN7l$m2U12ncJaGj9n8* z*icEl5q%q>0M_jvLfl5Uv(?343vOiX}nzcDrS#**Zv;0KHi-rEkI6Gh&+z?__*fSeo*9vk5) zIc|8ksRi6BTa}%|jd?%+2v=H=zQZ#*>bd#mou7eW4^&7Vs&8tp^Ztjg31|c9updL; zKPWeclxY6afZQDAh0t0Bez13^2m0!+2e)tpfqkV%fe?J-PCB4TLxLN&0uy`yiZSXF z_>XJ!>^lEE-O7){0^`?z7Tgd*S6}JzzUg5 zR5-4us3YXvXb`W*qwLp6y5U13%=%SHH%0J%@Dd*2P`;#_B3b1rE^Zv#;wjE+EOKyQ zA}(ahq`Z04gZJb)G+SnaV*e=CB=|%xc$Lh4in*h;cn0wfZwb3IjH3OaU;ynwv_Axt zVE1IyvytVne>4c0`{eMW@Kh3xeup1LI}mUj9WCFzsjsE3ec0nkoOVs@rZF6e$`y#A~1>3nVlUZN%uSb(jgZcK!`k3&hhHT9zueTCLU$YkCaw zZ!9LBe`C?5kPQ03^cyOK1${_JN85-CI}Nhxv}i7*ZchnsGkg zH?ex(SW8dxvHmB=>x9qX=YCmyNd5??3(_B8hh8k_1|;Fru|2r&VmUWLjDf4OA59Il zl$0i^JXiZXPf;MI%o7R5b+J>#zTnw}sXS`DOIdr$s7z2o-C? zo|gzWNpR5*t@xp6a7OY1cM+*j1~HHl%28nm>)?V0ett62QdV7rNlXz}@ z_Ar!DC}vvFA6b^ZJR^)f*@<|&7KkHHG{V1qj(U_Cp%??jtXT#u5WH$;Co7{96Ep1@ z-fsh}DD(`pe=|I+8RpAhf5su>?{&b5D%K(4a2L!qB1Z`yQ@nSLT%&-O%6D*Q!^?PG zHu0vP@fQ({i1*krzd*o%bb$v275G6>NVK&F%_pADzbf z6#i+fJMNz*e-U&x|8er|k>48eDsr3exaxCc*RcD&be-@4(*43WcnD#%#JikJ|3Kb& zEC@1@@{6J(F|upq7ulEdivUH$A|4?&G1iz6kNaXwYvvdEVWJ3R7lEa*V^=iRU)tJw zpjKv|4c^*5R$64)UNblnq=)2w8J-!+9943P zKs?J>0M90(#15Vrs2~8#)&e18(K>c)SK0o))~nO3=7p-xfxiBdM^KFPg(v=F=V0Yj zX>Mw#C8NHwqOR^&ef7UXlh8Nlo$&WPg|;U1wo=XG<>S-`ahxf~X&!rI9Or%+Wh`Lh z`0Ut?SkOkANz5(x?gJ82F$BCaW(NqXBxhg__4+WlHT6clAwDiFCJfLEmgELo@QSbm z5u%8w1d=ztiM;W$=H|;fJNc)3CWP$|nQ`OWdopCU#Rm*E5Ij`hIsgXkJxrfI8XTpz zb6cC*zQFq`uu3{!$;W`nm@mIC2VX&Tr!z{_-U!`#!EWNUg4BND<9?&=7bm z6498fL5AUZIZL1*3Ly6Jf03?akPKk_sOOANk*>sx7*K$PA69~`P8L*3^bfxO{qBKw zb49eloNiC=?(Jox?5R8JuG-(aDI%;fJ})QkwYv9U!BhJ}Ap;`OzIe(Gh}TDj!?nmw zVdl$M@`(VDwFkFA{6-*O$tuWN#Aqo30PZW_e!V(&b65K2#N#Pl5|kTTrb-293gBg?N(?*yMO65EKj)ND(78 zL+MjoVU;AIB2sl$0;b{F&w*J=>db|* zh6Pt?QD$N3jBDF?{9p&-35qw3msNMwyNmwNCs~RC_DOaRQ86m80ODL-%7;U1 zfuJX^=Mx_phY;cO>x!*{GRZAVPPHFx-LvCYv$KR`&bjIUWOjCU^$&qbUe{Pr(OAyB z?{CRZXY5%BStKfB98N%%#$dthaz&#uXza!i)N7_pC1hDZ64Sn8q$C+{qma0Utkh~}0FDjxbW zoB&WM^(f>MY1Z1}dPi;TjM1~%IMaxdRll?}JDSsTx(f=rbMX1Xtfeo#-&j)QeN}#C zS4mmjq0TQyY|hx6?{@U%;G&Y5kx3wzWzFB zk92r}k8vL!>`&y7K+d%;{YCDO|A4loNLl1n08S|s7R-Z+>hf@@M@z>SduKA4vP_w& z7Q5ZI^NYv^Mv$@CD3nJCiI6rL&S)k6PDIs89nA;oYW6iXEL2r)udZ---#gjR(Nj`B z@O*Q2PurcN)ojmLUBg6PuBVkzofu%7Qnkq#E{z?PG6#pW z+y}?Q@wA8=BnG8aZTReE&i3luVrz9-=5TUK#Nu7ugh>*xOcEp0I91qRDDY);A<7axe{a(8qPiA1qP(g}(I`^loJ9omUGP z{wRQhm<8BH3P8z!d~oO(jBNoB${!Fq#vb;zir_ICCd4e4uqT1h+sWEagdFzazP|kM z0Myq9_XaXgiXXFBIq*wUA0T!w1pKo7ji~!pG?QiQ?P?xws#w@oLO>YsW~&ZzocvsQOTrJv_Wqp`&gFy|FZm>bQS!Dj&Vo^ zo%^p_KCY8@Q=chnfaA04D4uy`jbJ>JA&iN}9qi|V(NzIJtj7<+zJ-DHW5DG50PX_Z z1o1;8ugMUA?_LEeRfP14e80$oNcQq0kB$C%>euXEFCyb8kq&(zotEN|WWrdL-@Qaf zsPL-%R(*&6)^PlWoL(ASA1PsnBd&rIY7ef8GK6pn5zF+wf6YyBIEX}WeUYs3L=IPb zws{&Vt9z+E?_1M0Z~nqV#ZG78=aun2!!c&xxF2B;@bPU&9?z3<7-F)AU7@u1Fl$l4 zUD+)7dJHx=QjxwQa%Ny7@dhQh;(aHmh5W48W70;I?qK9UR>uo+#*ze^O$N zKfz1+hw8~;gQvW5EIx5Z&D1_mV@-2Eee}s;i|=%%W#;8&7w+B7^1QFLb!_fq;l6Jg z*Y>3|sH=!RXl}uqf!V1D6F|h)eIPAQO!>wEPfU+vV#lFvRdw~Q-1>;d+I4d?%>4s{d2(oUXLEB& zQH{-%Qvpy3G864Ni1viRQh~Pt(a5T}wR3@#s1+rMnrAT@!wvdq+)7*7EmuZGk`feV z6rfxk5xxOU+I`?&rbF{meNAJBsZnz?f9Xa0ykA{z9Lf5>tI~mYkstpO;m6PLyXPsM zo9u3g>t*NrN{_y4xk}~oB_t(BK>jNo`u~EDKAbA=D4Al*0)N|~`uaoUqYvu$cm_h6 z;G^f>`Mi#-td5mF`nSA2YdPtG*?_tH4s)?~&MQ_PTFqtqikw&OPlPw`$!~na!_Kg$ z{;lpMIO~W84NJ+~C+D-hEwY`jyG-P@2IB1&dB(>ZV1s>7EVm522m7F6PK;6}Rc!+W zLBpab56F6^o(+`siGZmRGcr@GF{@=gM7^HZ9w>kll9wIO%bbL{$d4j~;x8QyN62ig z+*#)ts&=v}@6QtwcO9d__du_c${ClGP&xIZ2DcW_>MCBJWl%vR2q@bl2G{l7`R;5T@LP6l-6 zKTbBaIL?&gA4+avM%tjS-OS^Ytx$U5* zrM`90voh4VJvDQmkNx&C1UF*r#2BrX*|O9CyI~e%A1)!>8@v*oijP!qB5++wsp#PJ;y37Wk^MPvbKU9l-{K`Ga64_F{Zs#;}e<$8JH8?ic+)(Cp zt&k_j|Ci)R?Dm7(_8fSKg~jwZ?e+E5xpk3^wE^!9{P43o*Y;xrUI(yr+mz*2OHFP4Yrlkx?C04j4N zg;TB)*tEq452=I%@d<5xPx1KN;jIk80e`A*sLt^V?(gW6Nk+}^Qb?`gvPz3>zmYI{UJ@t-h)-zR0%G5g#9=gIpRP1xsyPMEN*;NSxMHY8cbzU<_h8-qi z**X1}OYZ-4@PPW@(XWGkykA`=9?GkDPxUeMCou1GRlRd1#L8cYTBwAk^RD6oJ}@0J z!G*lf^DCnZp(^6G6wT)RyiZ_ZP;i#ZqmzOY5Jc(=&xO*c%AGwrH@DOKJNQbXJfAc+ zlvJInu3~jP?w+ynu?i7N>ivr&Kf_*MQi1i0y)6F-%-Nsg8TtR3NzIC$a&a2La1fz{nyo!1oQu4acYPdl-`ZH*5<+<0$XqW9^>Qm4ymhQg@GrVh&c zpgmtWAIkB|tPYNai{nf_#E<7ghx-DTd3X4)E8+JG*>(szGV!qt_&!SE`=G2iL@Z;Y ziSTtu&k$x079r=-6f7>dwA2~{RFksea7UC48GM24*=Dx}xIAEsXoxv4aHmJp{szWA z9W_=vR$6G@UQ^$Z>HW<5O;tXpNA>=;Zbv!!XT62lJ&q7pX%la^XdmIwU*Y{`vWFiR z@`mdS+;)s58__R^NQMiWk}1apAE)aDANT1P;y7I|j?*4=^qkdvf1Pri?1&dQE)HD= z^T6b<@^K8uamx2ma)y`b>3sB#Dm(br6Yd2cSR-e6B|UKcc|jHmuY}iHPg<87V1LGDju~F=z_)p-Xm&(sc zW(oN{DM+ZN0*@5V1m%>#L6y`;Rs%0CEUDV`_V5^6m==k~jskx*ERI1DKIYd+3DVI= zrvyJ&Q8Rx3)~)xC*Hk>`y{CT|jxxCRH}`q(8OS@;GkwR<&>hn~r}756GS2q5U0Pdv zY1_cr^e$@GF!T?amngzXV9olfY#=g@_D@6_G@5u6V7z>SH+BHhg+M4Z>p;Jvq$o3L zm4@rq$N4v$rBWMylT~gXP8cfiqx>h8(Exyy8l~M6G`4w)HLODVkmf(>G%@A$C zw%@QG<3NTrKB*$4r0F9Vo^RT%k=oK}<1aoiBEN^qm0|ZfDintN*y4f|`CI2aHx_yd zGq)O>qoOmri{xbXthYk^4VtrJVq8fX4R+A5m}3><$r2NHhekXrM?o zTsDmfl>5i!EiIS#bkk>d@APzU-}H1}P~TmoKYo(_I4uRCwZUkUM&kRf zA;4Dl3~Ul{RaI74!tpzD)INgRqY_ zAWpuIwn*iod6VZsJ!1PU-x5i1#et*YlyvmGb3?9m815h`2op>ck79;q2Cb?{&i4Ll z8;kZnjmolhiyx7vkG#Tt>VK6{#R#-)mkP+Ip0FN1Mi7s1G5~lW*E-&^a_CZ14TjW$ z)covBgWX`u$h3-~Q;=rj6kkGw>;}ZpPEL>B4y+`V*%5(FpcDq~m zR2Ag@qPwf9xu>tO$K&bg86E9mgZ<@IBl-CwRptG9eQRN9e|C0%X+awhzKX)a^76vM zip5v!%Svl&OUvrfdou;8y3!sO% ztQRQp&qUSeCi4Lo}gJR9)?3eQ$)ptZp5dK~B339xRcFnt_Fp*&8t z)b3;4h#&Z1#@!UaxUugW#vlRjiYLF8ZzVuvDz#0Q`S+^6V}|{#n-?!yb|(3S*2P$N z^;)bOx(Vqb#Jgq9D|ij~AjTsbZ48n&ui%45!R+R7(G{Q-aKk<#87kboL_afJ z^MVx|5mdDnM;!EVM6wMs(JtbNvgTE8D;O|#C9(%Fzz?O{5W_6VUs}3NiD5RtPj|ia z3z(NtxVBO`TglZgdREs`CEG3i6z36(CeEXR$MSgt&L8IY!|qz+d;*?-gx{}U^Lz~M zKgZ7_UhM++ALZxc);w>;^RMIYkN2HN@0QzV!Sm1X`xAWUSGC{Qe=tq)epTT8$$0)X z{Q1B_#QEjtgHKmrtdC)=SLvhH0DaWQCn8$q8gq@U839*TY{o~-Dn3ddVhKOI^82eTL3y4wy_*JWJO zyZ7pXhM)NK)8a~ZX@O(gkYlQv>!(!LXN2o$h;4{N%nizoeozb`FF4EfQ`o9tb`~Bi z70B$AJRx(D!s~vsxAMqP@3ol)eaCx7MmLpw*?XG(@!4hV)LmQR=X8i$!gySc_FPOi z^%cHdlg06@Rr&H0W8ROkiaGC#?+>5wsirr8%hNnoquUubc9r`@hglt7;lg z3=JJ`sIJT}thuD4^OD-4f_FC!wYLv%ZX24N9ojTCJ@kRPX+cFr0qLf7Wo5OsWo30l zr!uEgswJz?MZ`sPQ(hm`w>*7xfue$-3p;evz|INX6ghqN71}A3ZqonARA(=rM-h#j zQo4vH8bKWLbE_#r&2)P8Eyrc>?W zbD(Idu5PMGeERwKAo#b{Wr9Y-b0HB`0R@gQfQxbO2iSUWZ}z~zB0w|ZBaN7(t^d9H z>ej`-vUG>3qvg`7*7T1Iw(cr-)ICl5YKFrs^wq5HqQb!oOp2hJUH;#qtFFPweX1&Fg_)gI1U8eMfOd5a$pSvG)I zsr&y6?8S3&_JZ;LpVq4lOCMv`GZ(y!QRnIOT}!vI-K-4fqQyCAyvzFjZR{|=FXp^+ z33%oaexLrlb1``4IesqoymNHlQGPD&ymMBZyNLGylJCZy1*TfqDOw6bkKpO-u#*xvm3(wobn;28B9MxQFUriMUkC zmkbCBBXN3scDA<{e?=}=5&dcQOpNc@Gd{7WytBEvqocW*_NOIB#cp=Tzbu~$!r5-` z>w9;6p|rG=Wt4hv#j{{#f&M;))&om{ufdOfuZ^Xwv@9GxBMH#A=!{Os4tqcUm^m&# z-&z`(SG1?2KRN3}X=&D7N%km%Ss!Dottm`)rtT%Vn1Hc6f_`x9*DsfR*%tx1MSi&? zP;X@75QJj!XBHtm1)7Q?8p3W(;*5R~r=OB5Y8@3) zs%vs`GKa1AdV6|&eLafj$5K=aau#ITTWw9e#NS8oU*wn(%5C;n6J6O+c=*l0xQ~wZ zJ4WLTaqXGKj?$)maV+?|>q_1~|Q6?S)_^bme9N43$s?u3pS>br0&{AAe z?XVPXNT|xMsw${VxXENNn9T_kKVwf$w%L-C0qV-A zw{o7XVxC2fGfsItV3?Z%H0dkm*}QU&>oM(xnB%OX;vDv=6_ZS}{vu6sn#siD{tPUZ zT`zxCU|!59FfV2lm>1}>iy0a1TNzBAXVSxIL5=%?mDQn^OaW8;?2Zit@H;%~FnsWO zTIT`$jwl9GKMs^uraW^i-`sDR-O)ooEW5+|*5-HU=Mj!4AP4t@_#N-yB)ndtaJ*`n z-383Whhukg=o@`PcFH)4ZJDehZ#|hTjjIca8fExL@?2#*g2R@mua+GR|MapHJg={_~gb zk6QD5*Z)6$7al*vnABj@c3|D8q)47~!Tsyx#}AV!PND!#-sD!ViSOgf{SM%cw6B#( zn}l^^)|Ru+lC0Fm$eV$~lV9Q=s{dHyyk0pEnTqpY#r?@E?)ROCPOJL;8u!O4_k)U* z^C>It_noKrzq7{u2H*Yu^DFN^|2!pfE%(oN-r~RCdj5Hcqe!+u>kUE3z>LDfHm(Z| z2@cX}RWe%-9>Y`B;9g>NC4%?vNE(G$%@Gc0)u33Z) z6r4ezNKTbCQg1UJw;MMajMv_XqA`hxauM%GG}m#^CcW2;&$scd(sr`%;#-#L(Y|kh zwp8#c5h5twCt=CJ4T6wIm8w4HXh!azMQTvbca)HXW6Pf9_$ne*?XkWXBQ)B;ILN;D!cS)y!TJ=z!v zLrM8B#F)V-C7m+VMV2~cs#BV}NL7a&BZtRcdqYfP%#-*#c`okMjre|o{{7k$aZj?Z zJk$Iz{%iJqVZJ%tVBSKdIM@UEGn% zV1wGoYrZZN0c<>4g(l%Be<ehsW1RJI=&XTGPiOoQ;hEFkEjsWB7 zr7K9_mkIV{?B-Crud+G)c>mmG3AW@k`;^{lBsV#nEq&>AV-C%Oq zZDnZ&OTIb1ggSw-!`iB1pJh#)FZberO4}=akp`8HOVB?->m+}CE6OK-X#G_6>Kd_D z2p;8Y6zhKlk0Afh3LdfaOYp@jIbRHSA*K&}5f9!QAknf1z!Ft0K7J@IJ;UCkR!Pa9 z(@xd7NYi3cAGHs>T4HVD*3W6Bx;#XavG=^|Dsk>&{1@=f+Wfbb#Y-=A}J2X+Li7a5uk|-(kwnMThX3Uv_~h2cvtR~!bm8Q z292;65sIk>Z^DiRQ~<%e^?M5o-;>|!_~)NHM1Q1T^81A}lPS-VkEAe@@Bq9=*_!~; zOwVHz3c@4tkY_qnAszo5*PCyztSlKW zcRI_5XMTUpHGkN&^{p$fd<((EG``7**}_TI(sAwK25Cx_L@r`eSs^d1Vg%8^GG?_yWtMHnYv+hpxF{2S{~`VDJf zYGJRk=b^{#N4r({e44-8gyY}h#}Dx12k1EZMc2pRxQ6&K53q-xzET22 zgcB)8LPyb?v(FBmK0U~SpMLr)xE=S?b3^$3SVvHUATk0UK>8^oofN^05u}qk9H`8r zGvOg;&$fI8kHI)WuVo8d_dJ3tRoWxclf2E@>?JmjHDS^o;>Qmu$8*^a*n<2!96!vD zAEx8f7wCFl!1WtQ7uvw}V>+(b95EK^Mu6JrK#y*Zu^EguT`2Toxg}YjXtNtP7!9Xx zmgi}6Mo7(w5 zsmD8|kFz(Bn`G1O59t8%v0wTY=qHS1BheOWh4ODczL~i86@Kjje(iw|xR#!qEXRW8 z@Z43jO3%Tb0XT=}1fKKT4Nk0^6?Q}RvfYpuXg36%Z#Q(#g|`MI3H$VNsgeV}c?M_bqid1`n{$WLNL#IZIg zHe)T*fRyYNmca`d=_x7I<&%zD`+m1)UuK5IG8}2K7>)RQ-;o?=i%+pT9Jb7s>MUnU zLPk;m5pdfSX|F^B7K*k{NnT+S_-z5vNMwzx)o3JlNLtpxof92}J7E?OWdtal+$v zs#rJd0Vn9@dK^&U6ZQb-gI9qM9>ejE^5Y-%^FbH*AYFfkA3x*ggAL$=RXBc@A3y8o zgD&tv=URL)8_9?k8Kk$058kHdd@v-n%E%r~jyBk=#tnvqg@br5_!VmaAFNuF59TiO zLEp++=7ZPSv>|C#iIIA%L9VhHHzXJq4r-HQjDQ^YbC<3HjUK~uLFX64Gw}4{)p&-M z+DmxGF)Q#4!~8*A3YyNJjWLF8MSLf~w#w$V6)n61ov^K(Z=rt_kDCa&p&=~EF9U_; z1mTAa;Yb)`k!H)?1Gn8afLxeYyqR8QF0mgJ>BVfL_8e=LEl(@hlNs0OxR9V%aY#ZPQ*sdxWMjB89lZU&yFIcjBe^sPOpkC{m;? zWSFkp;vTyh=uqa%zY%nbyE^N#*6NXSJw4|}s#`D1s&3ioeWPT$x_Y{#zoV+ashZyt z;<SWZ1_=p@YpTw(EXg8-sV|<)GCNd1w z@!2})2b8BMu|Coi1nd+aL(=uajkAgUt=PTk&4wwLt6{F9Vn?IPHDzc{Z+of3XB$)+kbsIv1k)TPH)hOS`j67Tn>rSqSoS;n>X`}llo(sP)XT7>{kqyfQ6|UH;MB%mbC4B1~AZ zbAF#D{MvhR?wIi0?g?60VC#>aOUS7*hI>OpbaLoAuj+GKt?c{jTXt5{+$EtF-tF=c z#4BYB|~X5)=T(~hl8 z1O3fgoYTcW*;G`tsjzU6{{MB;Oxf1lxY%uFv!?Eh{-)`&Ed}|rWmAoVkLCB}=lApf z`|_zdX`snEtR)j_2;@p`SCcJX)~Iu=i3#hGRIkZ}+miH8bxRb(+LC1y&EWZ}OhVGA zu$2K~DJCi)vZ(O5=xd1iXF(?6ALRk5jAgdm_nDh0TwOS9awNR zAGcdAJIfnJs`l)1m29`CbPY9+R(l6)DqSw7s;#e{-x<8Hpw6+hr(~$#4o_{b>ev~) zdqGZ4=q%9uCc@t4Vlla4KME>K_aX&3GbiGQ)W`ItL-GUQMe(5TZ0Y((A~kZbza&cb zI@D=bBQH}UO-mtCXh`TdYMQ|03<~0m!|h%V7*i;y@M9zZ%e&YOSi!aYF7EMdB@EM$ z0I)PWD>EY@J%RH0zkIz$>EE?mp|rQ{A6!tHVTh)9<0kqzuno< zvG^zo3yO__PO&nzi3JuTIa=ovHi|4f{*j7%9?TS41Kd_9PU<*Z?$h zJ+L0NQVIvcQK!?6@wq1DRzle}Nvh{6E;;Jkc*PGK!%2jLV4n~FTU_Xxiom9-CQ1FNzAb4X-bDf zhIPTyoylbpRztdcYfg4ahd0YtijjS{qg?zO!%g!BrnCn*XE??E&=CUMwG0(HQ3jXh zEGS4f06)5p zC!6s8J}p~@^c}xtboArnp?l_KM?qz~YVj>q`t2lt!$MnjarrxddX+zxzlNBsEUDMk zMbxkk{LKUkF=JnKbxR130l}P4OB(F&q|<1pK?#cffWZtJ16ODc!eC})+O5fnyxezJ>2>`Yr znqA7`_j`YpFgY_bnZOeEd*6Bq>+o6eSL6F`F-(h%r7(gaAQPp5V~k!zT3iZ2pccTc;kqJ6 z5&acJTcb7nk0O~0@gNxQeM)Hzu$cERN1NSQ{|`LzV(y#f|YQyp4Q7|M_D7Qeyg&TMPZ*T$C zstPw^WbB9L(+mqU6J})9%hp^pgqE(>+RoBI6l~ysQKmOq1)ok)*QXEX<-FkltdB<@ zbm)CFcJ_R@X~~X{j`V1kItT52e_jzj4*1}ubO32`pg)WmQ*h+#5geHXAAeml=FaMV z{k9}GlBe)bzkXzHLF^s;p=0GBHtR$I{$ephvG;4x+%m{U+dPC#XsZ%uIKd7;%C8b9 zA{6De2jRO5RtjIxgk{?M-6AZo2J(BwED5c(B9@?PmKeEaX2!z>uFy1x=S;z9xGB_^ zMp!Ro^4eP!#((oq#7L-pKn~h8B`TQn`33m}2KaNVxpO6FZzmXL;?Kj%*3z0|O34Wb zFoDGc*y0mZ#+Eyl(Fk{UFu|e^=0S9n0j5g?3u0(5v%&eu8%_Kk{jCc zDj6%{Mwv!2H&p$`tK=W@a+#L>1sMtCJ(Zo7@VHfhmE)R(1fT}=p06*vfgA3jA94!8 zh~IZ-WKhdFCD2SPHkifo9Ub!QfT9+boDTN3-0d@W9b#o=0v<4?>-rMTx0*_8)EQUT zM@Rj1=1gr}9kH3eYSsKb)n!|^4jb`PU+s!B1-_(6XqNh`+<|<)x}q+8zA6OxP5~`} zo`kP7vjuQ*@X#UD8WLDgV(Fux+i)_=9hq` zxAp-}wO5Pz@Vhh(VckpDz8PrKGLjC?+A`*OGso99S65PX2H6li?<$U!%<2;nXJVT> z)=7Q)Iw{IeHL;ExOyYnq?Rv~_n|`vx%OIk||#&@5Cpwp7McfkZGQ!ad(RM z7iy=cbUo-CXxzo*{+$#q8Rj>Xb7XW0@Wmj5;hGru6{XP0!%3s|pb>gZl2RnD&EY;? z&`(@c@4$_lF~YKF@|``u{z{Xtji4Rz^Wx*xxakZ&7Xs7;O`oR>y+0E z5-@`k3a;$dJ)9dl6gne}M{yC#?}ORwE%WhVLZr~F8s`C86&VKpgJxKQT8T!uJ;AA$ z#K~2iJ+}DZMI0UATSFeR=BAX-U>(Ksfz3%ocxVXc%YER{-_KW`hfuM}1_yt&Ht5Y~ zq}JNN2OCt**vb%rdk9TT5apSPl!x~FoUFrHreMx=s zcf%@5&XtTPuD`Rcz)yW>hU+f!y6aR|^2Xb)Gw=Lf38aH+7 zbnv>j(J$PF#GcPmTlhaBaQp^OXS}&}clkknrzW~Zrd7D=S9@U_?|pgo=S(06?L`M@ z6~Q%#5oFu;&p-cs1Yn2ya^oF!9f^(0=MywPj?7lCCDok9>UGc5pak8`_}C+#pgvr7 z+?(HhV8D56i1!6<*8-_Qr6Iq$I)!!d-P0*ZHsUrYUBrdBFHfoH@_QVYQ7e^>XEN># zBE9gyh1PVShoh5+hm)hn#5<`xaCdU@;Pk&!$Bau#L%=Sh zYT!+{Ll{0OMQGe-i!Xo4MP!^EsbFqMOI zJf17pVU)0*oQA(_Q^DX(BT7O82n@Eg)><&t8unQ9tXWZyJr*@(wQUNS&3p(guTGQZS=gMkf>ch;j|KqEUVI! zn*mzuSYS+EHqiT&yY*slze8CzF5=WHl~ED?pB7&nXSZ|La=mhjFbM6Q0KT?4v)vQ| zD1QOMW5cnqP{GxL@W5zd43)WognKM_nQ*$m39Kx1gx3XML^hm-xODd4M=eoV1I;#F zS!2FrU{1StS{AuO1O?W3(vb7&f2#6T)r*V1Cl;zaRW*5SZSLgBs4xDJ$A-{`I>*DO zh{2m+C(zG!QWT8TC_}qy)T_z-qBjQ5pD&-`Na-4v@wgJ0$tW+xT|-77{$BDUqP|h| z2I?o{TtME#khh5dZc$5vfqzKzc=Hj5k7RCV;=l#1x{|rtS1!Fo{jxkF-zeN%(AHLP z=SJ>-kf*kWaM;81+(e3bEww<6k?H2b&0X`Yk;g2*cNmkubHWMOGs;2*d8e9U2-=|= zrKa*6G}hWU=*SY#E|(@xLvZ-KxShFMx8i^Gv%onO;vti(Dp+#DNe4V>=Li|icXjHMzdz{gAJp>RVPj@F(Y)}9vd$R>FE$mf;~;fI^=QREXt+(JQVaNjSa z7Ag#P8-+Jbq67PSM!DmMyO;b!8O?v(>kFCg*4xfD%rAeis|!N^g29(vyS>Pl$5TAK zmYI?5sN+-I{z!XwWN*F#>e>s5#!v_`a1{|m&y3{aS~cz+*h7!0u2Cg9s%(qTq+I#p z>mF#mn{D^FaHI)qsZLCBK|42jx<9r^ZI6~aEA%)a#$6g}!&@%Xg~vgA=I~vN3gR^` zlH?SD6Bj-KC@1BX3{qAvFdE?P*bw#8WbfnKazU-Ugi`8`JBb_w1@=5RzwChfr-;(S z*~tv7lBqFB7|XDSg&zF-cM5ZnPLR=|6Xp8&`g@7y*!OdWQa+QwrMUX7YK(*E5Tg~| zdioghV-?X+u|n!dj2u7z%cgYe`x$Ti3a-@l&F+`h!G*o`T$t#cuNyz0Ui`cJqy2kQQARA|IPj6HBSiI}M!AEXZ zXOl3#1@19RcSv8MmvwLn-uYpXk;7v1M)>*GMlM3lzHmda1+eTfMPi36gd2Bbwr4&cw|R?WAN=WU}` zp)-Z7OFmtOX^Y;1RWjF^U{!(P7Wzwoq)LztX9dPrTO1lu;xr!`WUs$V4_=$~w>{hO z+w?;C)h9td0Yh`L{Nb+L7Z1AY^k{ibnR~ipNYI`2{6;;_>lh<&uM(mz-cmr{y7jGAnKXTQPED&*lJH7u) zDjmiPDx||NLkm5DLFXr=s|Gj`${c|)D{w8^vt(TOG7@FWZsqxzG7MoZ#^O^CGZ^*9iyKwim6LUsf~$=G_#-eq+Nl-oe94Ey?y(8_f7uV-$C&dC_%Om)@lBN#79YU7Y0jSWRb z4W#u>Dw#eoDQRGOVq&^_I)BH%#qpo~HCk`T@hdLxhTkM1Q@EhJ4Z_Hv7n96PEEqBE zj%6EjYi4T7Dy)nx4Ka7H-Oyn=qDPW@U`?CWkf(*=x{U8QoOQGy1P;STf`>FCCZ+}^ z_r&;L(gN$zDph2p9`mY9RYqEBWKv{eeC*%!g6+T23te|eo;r%m&wT5tGk<&>?3X_NgxHKyzfA^?8oY2J;t-)#C_(z~IBTZmqrG&yB?q$C|5EhDJ<-=gXYRYy z_o9E_klzqLMM%-bmrp$Wm4HD(L<#p!XC<%P;OxN~_(YJK3x|$Y?;}a0VT|zU=0UF@ zuPH(<`4{ZfI?KlNlh4(}kbedLGy1)Y(Lec;2{%If`d{q((>_!eX&QxatV~yQ?Ukh? zXfGejE%tJI?F@aQ93W(;^y5!9W)rKyixv$YMbcz4jPzrLA>=CgMlREk?;E_ZKg3ZU zAjpWfH5QHD&D+`+jYj-RAr%L$&mum&1jxdFF+Se9HXtz z0(6AcC$I2IZALB?f)BX~+X-`w15=Ujk*pv~;;7_<$Bq*j32aU&ZvULiwj1P zsf|38anyjcN?o6DnN^oD2($RVjKKlj$6$&u5$Uav9zG>?>mjnGggdJBkgcMvL_@HF z_Zm#8@?g@9jT+cgxgj4fm5EX3*vM@6%`1krlHdWei8uL3DS6YL@Ot$aD=9t3N~JIf z#t-}+bfrq6>wB!+vpJgz?w}4!0#47$oz82g$8^_DCw=5zCxR1nV~NxSNe$ivLlqie z%t#>a)VQkt1wMy=QC}r|b44d<-4@k9~S<@uEcwNd}2gf22N0;y?cQ2oT&T48&ac7-@Q(yO0Gd zR_=`uLro(etEVCVyKxPWhfeAB5*DVY?!+j_%LlwIqFM1 zw|HWyzQ((Gs1S+$#2Msn@)qwFD|rIjxRbyy)kWltx(IP52vO7(^oer3!9)^!cPr|O z&n92uHmk0Afc}B?9taRxWK>dWZW{o*5K6xMlIJGJj~61z1-VRJel}iK@nkbGMv)wk zck3i+dmUD9JWB$%<_NWhL%1FbcfngRPG7Nu!cI@Fan+(lyqr+dOZ^X?*WJ2H=o9(W zmRx@uFE8GNAmRvw@OG$QoHwsW*`Tf{Zy?{xL*&3&8=cGnOp%DVlxZ7hW8==-_vIpF zCASeYwimwNZD*IV(N-o&Xl@W17UOx^~CJPo;O?FhQS+)fd_ht!Fc>4TtV- zK$u3zTgC32DShIL$>oKiNyctfNoh?dJ|vV(%M!?MaNGU@x{4$YjoEG#%UEO}zapo#qh{PWc-y_6%%$GuWs-9Em# zeFU7cJ2*}}Hfz?=mOGCJ=0`{82UO+fS7U|+X?~;6gU@mWKOWBa!+`-x+(ztRZ$evYE3vg*gsAsXs$ zLkEFlL3{2(X&nQEaR!Ap3?IH2?adt)>^-t5uY5q>Yi{P*)&b?}<|tSH0a0Orc_|;J z#|=$09bOtdI7&4lGC0F`U{OwLRAKOzxHPA-)N(p7#Je|c)`)di`rkQ{e9(75Kset& z;+)ND+6Q|Zb3v(6U?Jp`VFqtZ8|XJ5wj@<1;+{BT=@eiicS{7{py9vohfkPNOM>%O z=aHGDD!-uW_D-s~gQ0{m>8x2nP6B5U!Tz8+Cg zl}Yg}P;^y>$Msi62Pb4i`6{CxX$)=b(i8J>=EGW;$dUj%Pld=KgG^-U`9!Ahe5#szP;V8&3d`@k1_GIfg0E( zMB$1DBgrHfNaXxPsOAp6PTzy1zYor3l*?z`21iE)_(SJUaAJW=->`=hai@+5=^Cs7 z44Hx9NMo5M4@Ce86u)MdG|xE4qDWT0 zzN%_{dD#oq)i0I@6)Y(#T9j9?c*u~&1$B|-2?^zqfnI*mS&4~R(Y`P}7Q5jfScnno zWh@N$ATS`oX)btm$2va)do|o?&BYX0{frHbYR#awg!dF~8o_zAp$PQ$fgvFr7a=hr zFeH$zs-PS0f)8^YKJLnBdQuTBk-<@veu3}YaNxo*L@NjG6MrM zgMzXG1G9pH6BC0ukm&)LL4lb80hxh8nE}yB!NEz1At5pyf_zBW4?3jxji5i*raca7 z14q3LCAbR+r{E};CF$Zm%xOF;bgZ13r&q7PqF&G2-v@;1b-EX=_u{wndO11bo&_|V zPfadNf_Mc4ojVYhFW4azG7S$|4(R76fyaVYh&vLnk)_&PWiC@{dG+cP2^bo#k0-I# zgF~!~<3r;YOf>NH^&8}=7&<Li7GlIlDv0~+ZDnU^)jj;VnAPSH1DXJtDqd4DcR1G{OfA#H-K3pAEi${#=asPf~F zD@kkNwQGfU4tC4SM!>nsyE-2*gI#PFAS|JDpzK_xo66^|c?#6iT4M`d6d8z8KEhw+ z$E5XlUYPt3%HzDr9i{>ubb$>?wkji_4>geB084hmg`v-lh`<#Y2oe__U|BGL(gQ3B z9f}%=P}ISAI9I;oA;>kz+Y{OJw)Rna8$(RkmFVH7M410{Q7+iG~|)g)M=q62CQOzMdgbX6)#pAiF=JL!lM(T zlQPICgMAjpAyF}*W@hgipst{6utyRfljn{TMvtE`M`dCQi#T`}GNw8UN-s{)iID+h zP)33=GcpD-Gc<%9B}ATrLiQc)4!MiU?=SKAoqLK*IX0JZY5hziF3$RMRSPUhy`8Jv_H4cl4qnDwnRdY*~srvDA&3sgKJ4C^X`2Aq&-mqW}ma&$*`x?D^Q^>*Pn22A8eS$Pq`BdoOFl$HB>W& zox;d7Ft{Sf=T~XF_WY`@uen^?8avQnaDQLzuq4>Vqnlm9RYwShX2bx7anOrF;b?49 zYYq;ZRY;}~tTA|M0wL}E!D|FRwmOAU$o-On?8*s6aO0z02|aE6kS_9ePA+v;@0Y3>py?;df&5N9$= z=*)*}*1P2+5cRWkyrviZ5pDIBTgvs2_w|3HRex$(d)_+rJ20c*rWAC%HiY2{>K{hO zpODH zU3-8L;6>jaP5QF6 zasFC1RTm}(W#@4O^F`ZKUhAe@CLT>it?3i<9Z-VV;=MmWSwK}JK%%6J>HU<|==E7yx<1GfdJ zrU7s}SvwLY^N~Q*;j~0E3H65TRmPZ-m%)$OfwASBovaQevO3gUFthfM?^@E1L+JY$W>r<_7qi~4RwHqR>v-!_#R3X8rhO1Ly`1AT|+;LF`CGo0Q4DmbKSra~1 z0%^5o8gSleU%{8j2iR8x%k~u_1Y_JH1^fb3*swg|n-DI9=(NNDx1^7xH(R+I`JwD9 zC}abCiTkTHidyO1%ih%91!ike0ghgdR<`;dwA$9z!ra|8HrWoL!X>&tAA2FR23$iG z#$KU3uer|wPX7qKOGmYFIIY#j(bLhcb6*>W(@|}lzs`LEn%Ab8jJvnlgMH?3Gaz*s zSDDZ~30k@~5p<2$6FGza+^e2pl*i%q)W*@T30>>iGtOVyg1&F1MVdI~g~K#p^5EM? zVJy#WDF_u z-Emeh`9#RIlbuD#Id{%Qey&q}%NlIITQ9E|SGvW{($2}**4C}Oj^lu+_mht_CG-{K zt-}Bp8)yyPc(u7W5}bseq7&2d767NN`1-0;@3&ScZ2P$kOthxfp01v~9lRs>tz|_0 zf+j&@s9FE;>$cdH6M0X^fgEtrG9WOe0gcMiBx%azJpUuUr)vq__?`w=S6j~aJnM>c z^?2UrYG>!-VrS>-Vy_K(d(?l(?J*aENenWUyh(MGf7Ba2S zQ~k4xxm&nHFDn;^a%Wq|-syRgy``-)@~_q?G%*;5HbSDDe}p-t2J-3*vI{{cWI9lY z-O~)mMzFS0D!C1~T%dd+P8LS-E$1k8hh40%*xl=t~yQ( z^6&>wNz_gnh5BbffqAFnD0^{cAj85yV%>%VZ?Noh5Vy7mbT*Rr9hU~mTSPo4)g`Lb z%pg^aM{h4DYwyUjF~NcPZVH#UK8`L{VU8#-Toa<1q=Pr zE#xH5z>(X#$P&$Wg!4rY^i9Uo-rmK0ZhuX_ z{uhZq9RHC(?i|zX)*OIjebwm+bm*NCe80d9G&O9D%wIr@t>yV1uvd^} z0e3m+Zu6z*K7kqN_XqkAXvF#!gL13b4Z%lVF;-yZ;LY?DtZ;K$`wH+^*fH!PTCW2< zemdeecZ<(?+55*2L;T6?c$fV7+H;@y!0)rnP}7S|2OhNcjQ8G0^k{kK9)cNm+e0u3 zWq*mXIXBMR#>@8MZ6n&Y5wy)myPZJUuJkueA2u2N)`kNfWrcplyM2D}b;_Xa$V?}01q4LWgU-lLDrJNr1C{;HXTdSjhdJ$RqM?`x7B zqup}~4PkP8-UfaAPWpJyHvBdI-?SNHrj=d+FZ~YIbmfEiXx^L$@zBiX zo;>t#n!<;i?>%^D?Q};y!!h3{fi9a1-R*jN^37bt_;)-ri@3`(n~{F-D!S-_{bxp& zs+-87&h_^#!$&a zjGf`FhizVpBvfy^pF*PbzcojR3?Z5#ehF&@;K|< zoxfWgQ>Mgv$kmAszGtHXhYswci0kd>ViszPbX1oARngC7o-)U=x*Fdt&VGdfphVArcHXbUC+jIyuJXLpCRm_O@*k;_C636m~|o- zQmgx8K<|&ZYw>FR_Joep3r9IRAs4OCIMHJm8{V1iPAJ2aZ=PVI4!#;iz`R}O+O}jn zRFZtkaRFk?n29;`*m$Iu<$U){pL~z>*!uFt{m(JWb9r*4*UH%5$GQm}AMbKmCWb<1 z;}LocRHRdbqFXsX8#b(XPU#$45gS`m^Orod>n5-EY%W~ul7}o=)32P!pm^9Y`hHDK zEIgpJV z1HeQpE1{nX4#MQj_CVm)4o=AA(={WIrL142>9S&kOM?D(aa4J9ih%BletO7>bZ2Ye zeFCSDtd#O?^^p50J^m{7(GV}kKGuOWnnVOTKZR+_>epHjUtI}ums_$6j1aQQD6?r3?t0g!i&Kz>fbb6Vc6b8KTu>c%#*)+zm* z^aH;JCcm=xUoKms+6a8`yL1k3-3-y@qKl)#$y}Y3R4)L9+7f0oU1OE##=rg)o__qx zsy(IFbV_SdB{%F3g{=jw`K_Y^eH;|F=GNLrS`A^RnY(Me%Ffo@T+ur=3AoFJ9I`}~ zLw50T$91gQ`ztt0B`=3u^r*#k1#r#fYPbOgn__wX)Sq zyR2oYp`BLEVE3m=6{=Tp1giqQU3z;3f}RE-|L5fV{pI}o+(Vj%#)gS?`US6(YNl9UJhF5P_uRs-yk$Ah$0axEY09SmcOB(bFJ${F(r_ic7|1e{L zd(((YNpai5%Zeguf%}Mfs4;|e$;q^155`Dm9Sw~s_W;hv%>r6Nj`DgNT6Mmgg0_4o zzw6ccZns83o`77E8G3fUyBp7<^WAFNL^`08X0XE3m+=oeo{YrYWpGKLf7dy-3=KQq zZJ`y^NiNr{^IhloHFWHJHy(9K0WZZ2y*l5$i@sfjzGa5io$qGTYWN7|ZL`(B1Maeg zJLGxz1ijy!MF|EbL2L;>kf!u$P z?1#VTmin(rZ?KveB{Li%K)XPR2 z;(I>IH$?fHgko(D#j@ZEbwC`*?C!?Ny-%DCtY2q7MM9g9qs=F$Yd&*XUS}M>B0Z*! z^O!!4Rz`LXG7sQGmXUB723+TLlbc$}(Y5&FGPSPp`Bwcw@qg)(Kc8r7tv7L5<_wLb z8rjP-p^uV8k9)E%b)|z~*HPo=-YeWn~`@XJD zceOiTI75o)7(V>jb{@YRbMI{dOs~vO-8cT*!-kEY!{ZBrVMN51>^)W$y%K&C4A~{T z5l)z@)8x=iCEebo{=**=T`wIT|h0XXj z6TrhRAijg&HUDMkrMW@@q)O;Tx1p^2!ESte1?_-XMH^~P(pNYL!@$#I0C=bZP~xEZ z2VgCptr2Pv<^d2#^98~X0HyHKgU|qwfcLkFi)KStp!F!L9{KaIT+UmcKM#vZsr;@O z`QSI!CLhfoqPsARgRY*#p! z@eS}Ehd?u00lP777a|-9zIYR!w*vU{{}Qm#;1>W`ud8U45O`nMgYU-$1!S#vLp+N| z@jL$Cf|P$N{%X`IRPd5PfpV7M_us+m`2G&!eJ^;(A%E5Z@erzXkVf-_{&+Sb9$4o+ zKtDY|6FhKO&;xz%A(Z3YDTGk~BkTvV5Sjr}0F>+l&D8-Njr_X+`4eUQ3I7b*kUz`7 zIl|B>%U{BH$UPpcY7l1u0 z4B;SNLf`m+#{RcpwGc;*(5Anei`#`jcq56^1IA5%{WICD`RVSn9H)=|w>;mvq|?te z9`bnvfs72Gl~wlx@a%WLVXTsWVb1;y$aI&}-Uq=>#JLwdARUA6x}HE^1RwA)&~q%| zH(4%tk@cX*zhhi}BaERx1Cn(xKp4Y3v6p-nzjYu=f(>mG6f6ec^e`CTKhy%V7tGoF z_!bGs6Czj?-X8`Wh4-3a$m>nPk^@_d^=!3ZMb8S+kduxP2LdXQ#s_gfLYRPj&f(pQ zg1NXF@1_Z1I1?I0-xpj7;zPz^3jaXXkmZ_-nfU&V5YPGx7A!!pVYBfbWz#>v&ol~d zI14>S4Aj<}{u6O80KOHJtS`#Q#kVBD7P-D-*ino}eO;du4CFoZKEVp_E$E*@H0m1# z7=tqxW59zbIFLl7=XgOoS%@|tdKzuNgmO4u0iwNN$Z-gq`Ek(W5o5GZCmuPzbimuG z$CVrcPrP3^PUt9nTZj5=5uDK%aoDGuu_7Tt>?hb^z3PLujbWjh?btJ#GbhCR0pS|p z4{harC$|r6l=t=*}2zy9{Z^yLItY_zRw92;`3IN zxxlH792e>Ias$4n=Vf%}-F4rH^s;>BSF|M?`@|JOBCQjKlS?`{j&sE;v39m#ozMZx z$2d|5E|@>VJK+ePi*)c9E?slO^DBVIknSaf?SQ_RzgQD!U(GcR=)(Z&Dj0)T;d`#( z>@_XWIf$DCcobf_eeiB0^4o>qj)>nM`Qg4q(i>|z-&?t8*3?s~%r+2wd^L;dp^xIdjy@6lWp$nJz8h?{_TywA`!5$&NPQ2tv&HlB0DNFj%+ zu=cM;Ukn9bbpzk|7~p-1`p~iP$cDWv^7jEAi+CD&|AF$l?GL(^$)8sUX>wV59()kW zcnM|r3V9e0MhxSEPKDe>CllcN4qbk*SVF(*^V6qC$kSquTFElBHsFHqC%=G)Q7#|n zGFZq$NHfNCBIYJ<^BTb$&sMYneN!aQ$3Q&0V19?|;u#>G!TsWqt%3_6Ul;a9Xb<3_ z7r+z1zbi0a`T>>#5&($+SWUoQqC?043KBIN79MHlw&67pwHvQ)4kmEikLg=7*BGrsTP$AHhfH*tTlnJ|C@=4NmH zp7z7|=!bW6g(TrP0Pm3Y0>-x>=g|jrCI?WA`|j~xcnp4ID!}JdpbW@J0X8Hb9uf<% z4o$$mVglC3Dq#Y^6@EP$;1tlGYkYWq=*tlK`B9|Tf&LqL3Tbbm?3>VA#UjLAq+}h& z={h_k)VD!yhZ$&x8RlJ<5DN&@w~3ra+;5Q%ejA7qdafU_2Ag1<8*AG~o=1Mq;~PTZ zJaBSe&Nq~|n+yWp24OyRK%rCv_+G115}?B3f3l8;%kQ|p2lsBsa(T>2&>=VQa~nYO z++=xfEM(WqF^A>=4&mK$$WfaFH{8G70KKvsy$U#n^^<@93^Zvo*3Hd$ejj`taE-eV zvN)b$LzJR}QC*((?|O#uh{J)u_hQe1Mn0kWmhrjI<>=6#a2Y#O;yb4W9YQ+l;wA3J zzNT2H1hfKD08;>sfUyAnejNQ8^rjd_z}nwD4*$--IdMS0`s?2D_d@{_H51^ zh{xCr1z#FO=V8p;fNs_U>j40~h)7<{D^lv&1%4YzzYBvGsv; zwD9Vu0MM?>c;a>DIOBNHm%;1I+s^CF-wy-C>i}2Ss0RReKKz;EFk09Oxq=n;Lg*9p z^?blY-bV=Ep~bM{Di+QOuL)7Y7SMS-0~P@I7~8E2S0nrfU?N}(faftD@Gf9BAe%MF zbB)(&7N7xVi{i;pjFnh~F@g&3c)j(&>#uK*KGc8Hw@?3G8&(Jw#1CVd>ouTD10PdC z9Oc{1`~~fQrEneEEG!aM3p<5l!e!xiLWv8!d!~^}GL_6B%g9=Iq})mNlcVGmxkA*` zgyzvQx)t8w)Xaq0GjA5o5?L#o$Ck5=Y#ZCd4zp9DiD)l+h{0l_m?74PE#eY!lek|z zDxMNAiq}Q8WFpy19#Vo-BvngerAbnov_-mUAR1U2xES~vL>j0J3Jqo&EHhYZu-Raz z!G432hW3UYhQWsMhG~WchNBHz4QCrJHC%1D$#A>jUc)2sP;O@AXq0S}V^nH1$*9d} ziP0*fjYhkS4j6r5bk^v)vA1!!ahY+waf|Ux<3+|RjW-xyFuq|Tn3$OqnpB#MHfc3k zX0qO-!{mg?HIv(>My4L738v|$g{GCJr%f-J-hi`pGc!jsPqR?71hXQuCbO$%x6Dn< z?af2YGt6tu+s!Xq_*%@iIArmK#aW9h7B_o|y)1jV^qSPGt=H;a+k5Tpb*k6JUe|l6 zEln)#Exj!_TdJ+xtU|34tkSItttzcXTdlI%Xtm91kJVwT6ISP~u3Fu)Hn4WI4z^CU zF12p5o@2etdadjO64HnBFTHsv<6ZFbu1w>fHa%I2c2y=}a0nr(q?xoxBEB-=LI zCAO<$2+Gv7dV$YH#$#pUh2HLcXIEX-t&52?S0F|z{T3d(*TU}?nE_Ge)y2*9B>t5GuuD9Kc+-%)i+-ABRbUW^L#_h7( z?|o<=i#}uf%;>YA&x$@56p@N@MU!HhVxD5TVx3}(;<`fZZsKn5?%^Kn9`By!Uf^Et z-sC>VeYN{G_e1Vy+%LQT?m<0VJbXRkJ<>cXJvMr5^Vs8Y*yDuKS?R3|S0*Ymlts#F z*=vT^ z0%F#m?e;q8b=>QW*JbZT-Yb1XpA4TOpK716J~Mn4_^j}yzH@w6`fm2!>ATcd*XW`?Z@+Zfgnc0BA-*o|-@+&(-o zJT^QvJTLrc_^I%V;n%~}5oQsN5uOpD5or-+5yvAgMBIurh;)ezk4%p|7!@A1DC%Z3 z_db>pT@+m%JvMr3^qlC^(U+ptF^(~wG4U~JF$FQ@F^w^kV&=q(v5B#*u?u2X#IBFs z8oM|4NbJctMOLl4-YniD9zML{ z)8fnGXU1=fKM;Q+{!;v{zP`9xqp9z-zVrGn@4K$=mcG0C9_V|luR6gb!9KwwAvj@1 z!mWM={jB@>_KWS8)32uA)P8gNE$g?b->!Z~`(5aFE72g)I?*lBKQTHnIWZ@(G_fwR zIdMjkcT#v#a?-M-wMm88p;<*ABSrKt*3<*G*2BvqSgiE5RqU3IX3 zM*pJzb^X`&Khpn9|7-nkCtD;tC;KNyC#NSLOum(3mg1P=nG%|koRX7LnzBA+Ys&7F zgDJ;TBU4qWS*gXTHK|Rh(^BW9E>As{dOGz|>Wu-yfV2Tc1F8o!57?XLoi;D+#z4=3 z(F4;5Ru60)xNP8-fd>bk9C$fhOm|L?OwUQLPoJ8;D1Bx6=JbyAqv>bUucxaASq}0T z6gg<>phbgDXSiepXQ(o=GO9CLGUjCL&Nz~BCgWPh?M$Ogx6GW(%FO1>*_o>{w`3m7 zJe~P_mSvWIRzg-zR%O=ctQlELv({&A&pMiQDeFeIL3VNW`t0r5XL7uAVskQb%5ui$ zOwC!8vo>d2&dHohIX7|*avgI$bK`T9a|?4f)7tAPFTCloc zYr*b^1{}_*@Y_#Hx2O~5<4Vy zNYRk`AybDe8nSjs`;fy!E)-=H6%{oWO)FYa)LwLWsP)jSp$mqdE%q#qE>15lEUqhV zE}mVywD@B2%@XSp=aRsZw3337nv$lHwvr_!8%jM&lS>Or>q?tTXOu22U0u4V^hD{U z(p$sKhPe$39hN*SXISO1=3%pktsS;~*wJB^%4nHwnRi)iSwY#lvTbDt%FYa@!y|{M z4KE(vIDFdh&BKolKRf(-xma#r?ol3Do>pF5-dMh}d~J!zMs&9=j8{s&@e?-EFoDr2HrjA%%qo@h5nOU>D)}=PM zR#jV2TT?r!c3$o3+Jm)6YtPmN*Qx3X>T2pH)h(@CUAMJvZ{6{c79$lS14kx~Y#BLc zJ93x>x=6f>!;Pvt6x>WvB99hy1}g>v|(Dq#)e%DhZ{~boNu_%C^VWi7B$v4 zPHkM&xVEvqaew2{#y+736Q-t5Eu305wQ1_KsqIrc9uXdiekA#k z&5!JSWX~gqr&&+Sn>J_K!Rg|3%jquDJ*S6GpErH^^mWs>OusopoMAb`Wk%?X^cjUS zDrbzI(K=)HjHNTy&A9q#?4wPO9+~Mp(_?1g%(XMGJm&vc^kZWmTlUz+Hp@1bHs7|$ zwgensU(mLqZGGG3ww<$zW>wExHS5BxE3S1@n&yxsGT%u~-#oS!zoV1D`hRr7ye z;IhDXLF9r33wAEpztDPN+QQKbTNbWdxNhO5g@+a%TX=d=;G)Pyjf++++P~=NVxz^W zi)SxBy!gbD$R!C&vX-n|vSCU4l8z_DCzd>MWU2qsB}>mPz4T=ClQmCndveb*mu0@o zYL;zWc5>N;W!IM7ek$jw(@$M`>c(=%<$=p%mycfFx_tNY{ZG@UYo2a;y7lSVPaj^9 zx?;|X-79WB6ZwqlnXG3DpQ(Ii+cSHfIsD9tm5P;tD`Qutu3WKl{mQK?cdtCS^7zU# zD=)A7{n@l<7d?AmmCLHJt0t}5wCehEjn8d)Zr5u2)nivrTD^OX?HZRgzH1`a)U8>t zX4#rmYu2whx#q&!w6z6mSFOGDeC6}=o?rg_y63m8Gh64lPO&auT0D zt!rI3`vv-f#S7&x)V(nFg{d#>UvIg-b^X~FOJCgf;+_`|zj*w`GaG_8#BWI3P_UtV zL*s@?8`?IUdWpW2^U~6nj=Xf{rHdPVH>Pi#xAFMP1~2El-2U>_O{JSUUZJljUa5R# z%PZ$M2X2ns+`4(@<|UigZr-|i&*r0>&uqTBS^cWTt8T9bzuNTb%2!WrQEVyPvU-d9 zwS?C;Z8h1Nx3zWauGi`7v9GtizTowvub*g-Z_j8iZEtLEZJ*P=ynTIpd;6aDBkiZ! zFSY;vhQS;5Z+O2E{YDz@psRah-5UqDiQ6)^P2JY@W)J1Z+pI-_V((x zf8R}aJMRwMow~bn_q5&Xcen38vHR*fw(k_bv+kX}?_B6G>Im*g=*Z|O?x^c%>1gX% z-LbRdSjWkZ>m9e=rSDq4>-KKoyRq-4zFYI|oOhSKyY}79?;d|wy(e%_@}A;7b$eR& zY}j*R&-p!9-?Mlx^u2=jHom9cYqHmVugBity+wQ5_O|amxc9=|8(sg~m$z^A{^0$q z-Zy)H)dBIqqyt+&u>4@=2OS4(50)QX^`X&+jvsFP@XDdYL$eQUJ#_V>sUPkBX#ZiO z!SUplV--1qbN z&oe%+{=E6~m7lkNe&X}1U#$Jo?913Mcb$kovG*&ZukyaC`)b}-XHJSI6HadYI{53N zug8A9xbXB20`&(xn;a;D?VdyKv$M`ya}GSo%Zz566DEe$n<~;>GnB4_>_aqw2@% zA7}r#<&wvx_)C?SW?x!+spHb&OD8Vf_{rj@z@N%~TJY0`pZ5NA`lpLOUHj?QW%07b zWyi}Nmjf?HUshetxLkO-{BrB%WtTT!KKZl3&*4Ak{XF*PRX=b3dH2tUe?Il|m7mpD zEU$Q6iN8{CrT)syD=V&Sxw8MtsVnEN-2TPr7u#O~e@W%m*@n*uS+L+9j8hp@h!)ZX zerL@r^D(eI$5+UJ`3(`;KYqSQ`%K_OwOIFzTO)*O-7^zhaCSl)N5r{V9Ez5cN`krY zwC>pe@7L>|jqv=Y?%5d62XxOknJ;{)dp5)KRo%0>u#_0;o-G7t@(k?RiD-;6+Q}Qb zX9C-cD%~@MtbCU4nL+;er0!WnoR4(R5_CvE>z)mudzW<2MtJtoJsabBknY(Owt=H` z&t`aDqkA?d3ihe)*+PgEf7Lx(3bv9`_iQDYNiiwonx-^2jI3`_golQODGKXr71`s) zwoIsPZmw!kQXJOVfn-kdToLE%g(tgGY=T6Ou7;bVF@R z)sWFuA$L>y-F?iit!bDzCMGyMG_>!Ktp19=cRvMbzukSI(7q_DZJy9DZmdF1boZ++ zx~@P4N*p_G(u7e{{_=v?5|x}hd2;a7QBx3uS2Vb)Ipn?{3hNstDALey#o%#uEt9L7 zYZZ7gx?x1^*a@{Yiiu-uYMT`;K+fPnS&H1I+OgWmS=tDJ3Vn^kg2RG=#Jg#EHdT|V z8b()DkFHfr2HF%=inRXuimH})g|0diMl?4xwM+<}&@ei9T=U3~+_bFwe}S{%aYB z-m6T%d%orMtA&uf0r`v(3h+Eq&an#hh(S5LJ)uHhCO7C7y z5n}UpHpsY9bSddxX}jm%y~PS$o5v!jNyu>&`s2YdcwcecYB4)m;Qs{+Q{~j!JoFe2 z20ojC(SJV$@Baq5FVo~!^3gFEPjx7HvYaa)9oiVY2S)%WV-cVCh5~&s79q!y0`avY zX)rE`$-?hk)SSmv2zPVN>Xt(wzV*a$7+T0dJ2LN8uD&)^z+@Hj<725B|MGfH#&2E^ zn7aYeFc$LV=N8~l(E~dZ&=x)?nvh}wO62`K8X+IsBZ13Yq|f?~(%frwbRBu1_*7n5 z1kDZ%-Fy5QK7+*yhwwppJ%x|p9z!G&F@R5SBVsIkNKA++F(c;0g7gxqiKXxwu_D&Q zhS&-th#h3)ONl-76yCxe*pAf@2i)Z21YNPS@Du3`IkGEpgWktis3i&a%HNUaBwbjZ&KL6RO!GD#L}v9n1I$t8ItpA?Y6 zq>$^y$Pgin6p^8%n3RxGGK`dw;iMb`Fdon726>ds z6r#yvq>aoHF2HNRpX71K*5{IWWIkB{>(v;th%6>c$P;8Kc~XcKzJT2QDd9`l57ooo z^#oZ#o*^sAvt$){j;tnYAd`QdtRpXw_2fme0r#=Qlb6Uw@-o>3a+DzaOkNS*fzDqU3)jlALJJKliVhEh?;0H z1PN|TV^pLPHK2wRnoZ~$OsN?)rxvsqwWLOdWFPTQIGrY_W#y5Y_r z1$Cz$R7pLl7xkt-)R+2Ee;PmoX%G#jAvBbR!3%o?jigaDn#Ryr8b{-4Uz$Ms;Wo4+ zs-pdAGEJeWbO24mZ4v2o5Y2$rBo(^2ESgPoXf8A#`8dxtiVmiQbO;iT&TOlma|pk=J2RkT_dC|sr^2%$B!7WOIWLK7WH>uH0~OdIJa zI+~85W9c~P7n|sKXo;HX1l*ZAkxrtMX)B#Vr_x72>@tKu=rlT=&Y+LdnXm}Sq-}H- zolPHy#x+a$N@$^TaZlVloXnq37rB z2t7bQpaK~dW?QXk3$zdpMC+I>zDKd{feHXU(-|c z8*tMz=xNws&7^09$LM#$qx392N6*vm=>_@&y-0tgm*`LQGX0rep}#;MT>|T7VpAy+hSh1K$e-w?;Aet(TYqGh{~0 z7W# zgIEU3WLYem<*;0q$MRVL8_WvX5LU#7vSL;u>=pL0QZ|g0vEi(oRj^7{#j4o|R>Nvp z9UIB&Sp#ciqu6LRhK*(8SQ8u1n%M-_!X`3!3551;3Y*FvVbj=jHiJFNX0pdv8=J*u zv&Y#SHW#|R`D_7O$QH51YzcdUEoD!#W$YW*{f^|dyQ>nud{ac1~i3lvh8dK+sWQyyV%=oH+zS5uy>(9 ze2?vA``CW=K0ClZU=641TE&mq5%vi?%06Ys*k|lG`<#8jzGNrZSL`JF8aG~i z!%nkr*%|g7JIl_o^Xz+ef&IWPvLD$c_7l6zer8wLFYGG&m0e^1VAt6{*$ws^`@B*8uA-aRM^uRJ(0(dKPti;C7JWot(NFXj1H?cv2s+UaF;ol_!^H?O zQj8L##TYSGj1%L1VxE{U z7KnqzLUD*#Bn}me#S*bp943~D!^Lv3LaY?4#A07DJ~PA5|@iliz~!u#FgT+;wteuakaQcTq`~=t`lDn*NZQT8^o8y zjpEDD{k|e@7GD*&h_8uT#n;7l@eOgC_@=mB+#&81-x7C;Z;QLdcf=0yU2%{2p14=s z2W|2D;sNmk@u2vjcu4$6JS=`J9uYr*4*65@nE07^T>M=8Li|!ZA$}#E6u*XM`5W=H z_^o(G{7yV8o)gcD--{Q-AH<8|kHQ*Zt$0cNNxUrnEM5_R5wD8Bir2({Kok8>@rL-D z_`CQo@uv8PcuV|Kye-}l)uIM&A&5lzm0pr08Ayhbkz@?5wW(w#nM)Q@FUeA}lB^{g z$yTz1F55wJl$<1Iskh`Jxk_$QA4ws(OCFL^@|3(JZ^=jUmHZ@sDL@L8f}~(6L<*I{ zq;M%hij<4wCF>nVyQ$bm4-=W(r~FZJy$Q5q$U zmc~HiK2BttHsa2XHO_d&zrb*MK8PcQDOzAObpod?6=$R zu-|FF%l;Yr-S*GgKWD$key{yL`~CI@><`*MZ-2=C1^dJBp8uEZU$%e6_Us+|qWzHl z5&NU|ui788AGSYkf5QGX``7JH+MlvNZ9iiFhW(rNXY9|~pR+%2|Cap)`?u}iv47Y8 zJ^T0VKd}GM{v-R3?LV>q)c!O3&+Wgk|I+@V{U!Ua>@VA2vA=46&HlRm4g0U{zp?+; z{yY2c?Qh!uV1LX0NBf`bZ`=QD|BL;v_IK>>+W%(%yZs;b_w4W6Kd^sj|H%GN`^WZw z+5c_-kNv;)qwwurJ*rpjf}8|fDfm6CJ{48{Dy9b1po*&@$W+*^_NWoHSM5{#)n)2( z^#pZ=8io9YC#fsdlT|{Esi&xRZOK{c*YYC=t_r>nHesI1CCu0viG z)RZc!l6r=^Rvl8;sq57Z);k~vq72``y%)ZXU_X3=!R7FL=L*Pcd!nkSs;Vhh)zyr; zQ5{w{sUzxUb&EQxZdJ$BZR)r>0ofC?>Y3_xHK*p)9qLZ?EcI;l9CepkPz|-Hnrcb4 z)UrCIPOBAlw>qO%)tXvYZFN@Nqc+s0+EUxZR&s>H+m~^$PW%dZoIcUZs9Yy;{9Sy;l9SdL3kKyg|KDy-B@Uy+yrMy-mGcy+gfI zy$fr>XJtu|5j-&X&u{>6H; z`d9TG^<67&eaHH)^)Or#Uu%t7Plo4xZ?xWEeGsmZ?^FL~z217K`gipo*4wSOS?^ch zvmiU$`n>wS`hohP`jPri^<(v4>c7?hsQ*=uYD@R%UcF04tdDD3E3I{(j_Q6L(*t@? z$E~ClwH|~VzgJloAj9XS@I__c(nIiV9>1xFHDosFJ$gj%)%zf`<}!V`euBP2kLoAt zC+REolXXIm>8I$VzDgg^SL>(hYxL9fK|QWhdIGX=p03k6qq91v9i7(&J*A7f1i3la z>O=ZEeZ9UxPwTR-=&G)17czEc^o{zkzDXZ}pIE;|AJw<&WBNA8<2j*E>RJ6veY>91 z^ZE{br+$`xwtkMjOE2h#Uery!q+5DfpVFuGioRQ)fy|#Zy{_B(tiDHY=uN$)xAndH z9OMI?*Z1q^>gVa_>lf%3>K9qBw!RD*T3@riXuZbz3Ot(nI((|H(U(zquFVQcB z{GkW*%k?YtgZh>Ff_|0$DgA2w8vRnj~dKtNST&0E2V>FI#aDNUM3vcRRx!J<)Ywa;)jQl zg8L=OOJ~zrU0!Q6H`?pE+&+4JYBaaETDt6VNvfrbv1)T=qq)7dyxO|2 zzq-`kYBb@&Ay|zxa#XF^06|b}v>Q~Lanqt#n#Kis=aoytRW32JRvF={$mpvwimGT^ zmC;vu^x3r2?{;*baVs+5ia+3jc4c6seMUrP9PRQzx$6pboz7IIco?p$W=ONxOgPwq z8F;X|*5Kjx&vbR2B`wn#*NNP?*w|pXSG+3C-y}e3LpHOgl`2XrJ{BK&>-w zmFj_B)`w*@hgo}dcxz>KsTD)pr^`+{m(Ij)>g)(KDh?bmH0Kdkr_{|ft?FhP=)lbe zOXOx49BY{)i@}tMZaLA(;iZ$Qmeo;~bCl&AHRP}ehvo|mQ`qIMpOWq?LMy6U0}Eu_ znoM;~daKEl*F;2(waM~=N@soh=KLihOQ`DFx56|0b?&4lI=j9!;i`$9-SptCr?+9b zZ)~rvHnwTxU~O05YObBUw9fkzU5?$lx!Tw~ZR`$5Bs;v&v)QUT&el23);Zo~oizO7 z94ndkmx|wwt4L6FqREOK!$3h}?q6D~L%;tXig6kDP>cs!oz( zJS)r#I*GvwGMs{|W`kKuXKFcO&%!REUFG^^!JZAR?n;MsQN;CUL4>;Q;Ot|q4m)(# zwE9$8&+=NgXVE38Sz7Vj@l;XWPBx0&9@-*$`_f8lqqVuRsct{D(YUu2n-7WHt|${< zcho$waCKc)S58b;FRD98eE%I?o4u4BK1`ImZkk+eH&>6|TpKL=-A*=Ew+3fTY)^=7 zZl*KHpvsd7#>yl{V6rj617z5;Yao?^iKI;=Ya&j7-~mC%a5=OiKiBdBDi8jccA9-=7bj`7JxRWUx11g1?92+6n^w$8A7Nkdl5d8(SWtD5sv4Mz@6 z-B1gTGU_Yt2CD`QITRjmhAI`Fj+onR*h ztKq|!m?2naY_#~{4g-r0JbOcjgH29SnOK#5dbr!#)o@pUcrm>k&)ia=PVPVhSh7=us9S0%H zt~nH!hq9;R+zVua5oBx12^df^?v%)svydqLbRZHK<5SUW!0M^nna;-~!NbN`Nfq(n zz!cul1ZtkE=0l?o8>cBZpTESODw-a#&Py!DXDu~iqt`=Ml+NbDB zbUQ=gxok)+$>wMTAq$*tbw1)bz=w-MMsNyc0M8>1OI;#^GU3q9zhTkE0PqsGD^nN- z8(-v<{Zc}sbWS%UjA_V?suA9(8ZELXi%OSb&2YK)Hv=!EmV(d>u27;#ng#-j7F3JI zr&W%;a^e;|fJxr?Yu# zpU##jhytfxSEoqB*r{-o1E-95wf|I?3R%(?n&u;?!>g3&lc~o}hk{t=Y+kJdlMRz1 zL&(;nD-@ocJbr{3s}em|Magufcja!qBI8?;OVvscV#2DN;>%T`OqZ*>JG%xQO9N+& zu?(C!wb5#=L$c1&N>i=UG^cdvraj=bFxkuHzzteIkishW9yBx?ai%? z_Sw@dwa&8E4OuM4krnGq?P>+LEdM%EQtiM#aLba_P!l_GJP-9EqQ)9!dCkEsOH7~h zSCOo;YTa&!@qSJ8c4dv#gsUcN(aj9D9~UkMT#_y)CHjTYlAPc}P%fI+c#WuqkAK2`Y3n!JxEr`5A$zE-* zpMv)wc(>Xjb#Q*t8Rh^@#7rgGM+F&7p{}-rIf9dn?o_}obK?5tl)W9=+?5%uiyE#! z2O`w14{kr!=D3ATn@>S2xnph99cw$-0C4h@)Hz;;=R!+F&-p%Ioukttc0MHXdMwJc z*YoN;v2b-=)>cjoRxhdhNqqnPU0$G+8(tyGU3dF;EY;v60G`3XgDHW@XomYQa{$68 zOof9dK{5tgg+m3Q@U;LDJOhYrxDgJ?Xjcwd@pv#PW?$u!NeKfeJTiz9Z}=uO8JQMq zghwzdSPr!X34+K8!|mmmA*7$q8UlyHV~6lqBaGX0gKan<=Mf8H8}P}j#K?SJu>d}H z1f~TZw1az+TsT{ov|YuV8i{Lpd{uL5s^MVao)p>}M$SkavMl+EPhAg%XPBW%g=Zkv z>b7O2V>V>c8Elns3E<&J6t&F7Ditw41_TZ*w%a@qh#JLHfgrX@ zE>r=QrMaSVJcigR`9^A@+dNr{B}CB!ai8pl0=ejmLIfOAfWT=M_6W``92y`J*PmmV z>=OtcHXH+0!~^djyx|O}^IUa4+`+?!>)__+J7$+nQRfMWu`eM$A%q&kX&nv>0uPyt z`?*VYGXjURFckV7Lu!cy?K*kaX}!Ec1ij4DIc#j)CO*m;-VXJpihUgw{u7@FIIm{Z zcU=I9<7)Q9j>8BQ$PA*>@=JhpQ4MbW}bF6X}DYY;;k6Evx|o z**!G}%rT07s7cg!Ony6GCw59r!!}?Tlo|La0Qro8+CK45%|X*UfpqarO&dKEwDA2- zGqMPHqzQkbO<6hs$t*S@fCi7Sx{4NNZPPfmxDi(aIv9r5fJK(zk7{k5h9b(5HbeN@ z0w9qSS}yHR5C-4C5J45Afre4vFG8KK7t27O*o=30M3IjEP(bvCJ{Zn9dP2X}pHB9I z%>ly)HU~`)=nF>^M1Ak)R~i#dJqCVH7|`qaAoUn{JYRxu5%zXei_yL`!+;;a$TI$c zhX_p1;P!1sPx(te%2xuYiyOBBK!?nW0pNq--2gDQXV$k5-VmTRf>WsRrVz4(!_1&M zapTGmGErmL+|3xc-PP9SCc;q;Ish8!4nK(M4ry@zj!Tf}!BGhJ?v^)r_nO6FgDEMIM0% ziOncqcnAvmGOs#e8(N#>I34Ux!JS8lusz|Ki|FFeCRq3VN~8^xXglyAd@F*wc>6K4 ziT9*HPR*Yas15gIE!3hG3>vOY&}^XHwFvMUCF0%%N z6YAply1=lp*2dtZ8957o`tZ{=INOnX@DFai-A}(vJHQt+rdKSt0UBrnc(&a zV0`;AYZ~v9Kp8Lt6dnh8(&%>QGIY@-eRSUx(XjTyolpz*2}vcr@UQ2f-qX3?fEB{Us5I;rle3XrwcMGJk&7^NDVsP z!p)_G7jed;%K?ND0=0<;fvBJzmLKU1Nr`1|S6oV!dpd$ss$vUMgc{%yrVkMF7An&h0mncS(0Y$<|!lM{v5EKsr zOF=s(d~a8XX$lM19drxAgLNC@h%?}ourt7cH&8Ji1fWEY-s9^P#h`%d1|%{dBp!qZ zfjE9>6OI!O(7^h<=M zl>CCwkyl_&k+bt0&Wr;&^nwWZ_10LktmrG5 zvG4_FtbbPY&GPFKCo6hpMZc`CpD$nZk%UM`UfDVPDirh;{nNf3{C-5=w8&3Of1HX6 z?PPqMlA3n1(jR0DqZ@W4W6&vx{3)rQ68Z8L*eUtA^iz^?l%!or^q1^Jr!4bS7I~cE zoOa6GKJVL!Gn}DbWGMNy zq{A=L0T+A83uT92FhjlAl@nUi4kxlgDg8?#tHX(|P%rw|#15RAns!{#pHo)>m-epM zk&`T;UfR3T-j(rl@@3ko%lPZkUQ!*My39je+SjFho!jSSzvX40ZCtfaJ60hDki*Ey9--U@d zfWR$S@(}@n11LZwagp1|3po)Gru9G@aAZN7V%b3O8Bahl-k`t^F*V4-W6^*u3vMz% z(yoDk6+%k3Y9eN*_y8#sVCv@QpOFnD*Nw+xCRsT~jwdWp(;zid>zb)G9{)k7H6vli zaZNVKpW;p>&xsKuHwxFGAx)Behz(YjLBR(!&?);EPVP=SoPGeMtYv6AnlZV zep%x=@pY20?MO<2BPs2UBwjc<*#kN8sd?GEoN5jI$==Az-rxicsP}Q<&&i(4iH~$- z?>eHFq|7^x`24)^bAEi<;S3Ha#iuy3#~snn@%8iNOaG4O=ZLSC3>-%?%ALG#2eFGI z`sPJ`Ui!;RKawNiND{jvX%-GAz=O{h{R`6Hl;Bg+Pf75SkBj`0=wFg{CDEfK;T$aB6FHot1-QuJ+%E9xB1dw&9QkmEQxQER!zu5du@%vWlc!*V zh&=w>3E*Ns{#**+Vy~*$zbf;=Ib`6|W!@!S-r@8bsF!hY`VHVR4o;W>T>5pTJtx$F zj~6?*(w>t)pkCU$qQ5KcIVlA8oY)bvsF;-q;tGvWIc|XP} z3O{Fhz&x?Me74TM8xs1+m;1gS!o*Iz%lGFBUG%9gT+V_(Ua6CwuXW-%IV%GGuCpiC z&0p_q4@tHi`oi6>6YtU&GGjaGUHUoIE_#=Kj@wCx*q7u5XQ9aR@iLDG&OWv`JiU5HSVdB{iVn4L3y0Hj>rA8Wh=7bWY|Z#?Fa<%yA5x&G20S%5vZR zt&R3%dvjUCXZ)z#I!AbP>olY-Vol$2dwYX{m3x_IbLBp)h;G8?6W0l9;diJJ)A|Yv zijw5bmG$L56iooiMxdXCBN!#=454`9L;1wV9T3Pp5F>SgkfuJimKF8@1%93wq z5G{5li0q90lAQq%oi`B_J0m0886nbnX}!HhIun38BOrYs$bB$`eGnh^L40f%0=W-j zq&^6uKGwI_HmDB*unz>J4+OanhOiIf!#;?Q`yi0}AV%r~A-3-nM`39>_0w__OmlRQ zP4i6^O0n;h*mp|oJ0CMJur&x z&+?kca6}7F)to4-OyWB|Ktv?jA|I%#nMR;%9T5T<}upvU}`xQyRFyd!gkXa!N zyQLyziJyfpA6yR@h6oMG{mUz-wn1S;3L%Q@j z)$cjA4aq^RHB9mlRYroUus31@M{^2zO*X0u*`lrcnyU?b=sd{KQu`b}XdVtkQuEE` z?x2Dj3rpC8uo&{;tsk2jNuEM>n7o35|5c6RGLNF;$7ek7F5$|EvRUN<&2 z+UK^9&BA4W+6Wr7D~)sVQ*{T3u=iT(j8TI?d4b*`@UX5l!nBa(8{XgvSG+ z21@gY`n~)D7 z%LqPCQKZjSKnZZ#fHMY6O91HnISeQPcB1rl3bOGRg)>dHln!CkW{h?S`?4y7^z<1y zF@Y0D0aRK5P*z1|6W)R5AzS6Znw`g7TfoNUDp;x7rvh5qkk3OGSknpyhnqRd-&MW(yL2^IqkZ_1?7TvA0m zm{5~Wv&!_qlqQ|#x4WXFy9!Z7_pFIj&2}`^Cbd9FJeW$8nP!9*6XsV*GrmR;Ws@4(SV7#zq^9RwA-wvQHl>Mz{d7{Hp0FSL%3w{W=GZEb9=G#jg{aNh+7 zy9O~e*wsF}v9i{RpA1ty;m#fn&^>-DNKJ0S*K)(28|WWwk^`?w(8dAP5 za)jPjgo^@=lH9tS$ z?Mfxwgcmu`fU(%sV{@LJtavI}fsq62!h{z|C1E6qr5$^*l0XWMag=q&zn)%jo8GR2 zV?eAXE+j5M2Rmu|0GQ>rxrJk+jpOrk$@#JQgg1R+4rrrj4C#Nuvs0d~j$edtY9`w% zK$Ddu%u%w^@Ol@Qy`Cm$;n@c#Je^9Qssk{Vh_whTpzQR*JhCj*NY%d7MLl3u-O9nS zV78*E&e`i{DLvz$LKVinkZ>;~8#t3>VQUm;!Ap#SlD?KOpUFmzyNEqbbMGo>1&xED znT-Z;(IqPvWBt2ed5k8<<`0fdc!Q~(-d@*RYSbpYcnSuPNO*(Qn{ZqJBrEe?9I@ko z#Q~e}hCs|PnIr)gXoA|_P<0`3VIkoSfzc+s;nb0nb32ix+Wb}CU@Ljwgtt3&Pgrz_Ga4fi?DPX3(Ft(BL z32$F&rw7aZU}r2ZOYQ8!^77P91j{F+c5E!KNbM*rM^if*%O|FG`mlUbYA1^2(^6ms zw!5bmz{1JIq}TIo+#nO)L4&wIAl}2oaf5huK-^&Bl_|>`9RCT%_hcB~m7rGw#)st? zj1SAF!1%CC!uYVf3dV=!0T>^aSHt+Qd@76&%WGhKSf)~mL$rM+Qef6S3yCVs?EdU>80eHoy$5)Jtx|WTTiwANqggfMq!0rTwYh85neNrFl<3xMTP1 zbLYVS;Vfnh^E@6;HkHVe<~fkYa@@x{2k<$U)E4Wl{U5<&$f{kREbe4`_Tg9^um=p_ zl5G}XlQfDGUOqK>*`W!q@c&|k1>FSpDVQQ_|AEA0Vg?-nnD+363p2?Xa6EHx#(<** z?@;LJ*|!%gTm+A`-`fpr5pbdhh;66es(8`rc*FPv-WLBqaau{^`KMFKRiuNGS*4}qDk4M<)eIFUr6o}9r>aTX8xG#U(B z+Otm1O(qV(*^S!wawP~2rovMP06Pq%AWYy%eC*0wNXl%1z7yVces5%bnXUzlCAx>~ zPx1BOKPJ(Nb#Hfd?${`tq=`fGlRJ|=dtu++&_O*udaQ#w-9h!+n)13^lv7@D++^?@ z`5jhLUTORSEI(Xh7htD9o^CLqlinn#SfwH3GQOHE-|+gA6&^Y+=Opa3N!WQjpc*(P zIAd`2)5`z^hCxS+iw zVCGv=7r})Aa1?+Z1a3`T?4g=t08q_s$m0U(IPxHH0(lTPi986*rXB)cSO(~s0QC@b zd+H(hx__*j1BmP9k*^2q?m#|*?nFL4nfK(hH%}q!&Ufq!&VWlU@j&A-xbk!O>5kbpc}(D>Rw>Na82;$8-^#W@C%{XUR$ zEsH$QATr#~ATm4`cn<~go<|tTdp?86{sIP({e{r-5X*ZJgUIk=29e<<=)A>@SCw*Ri};Fo+BfGKdVXOg$8h^!gEPW!&p)dAqJUcAtNYPrzET;F9%) z;}AdgyaouTnS(LE7cp;Y52P5 z8`U!+>D~i2RmUsk=o65l(I*k#pruIhR4Hn=Z5 zIJdKFskU=9VxQ=H5&VAG^n*#22000000 #22FFFFFF + #16000000 + #FFEBEE #e8ccd0 #d8a9af diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 02afbbad..61aefd2d 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -428,4 +428,14 @@ Find help on how to use the features in the app Syncing + + + Connection Failed + Sync on Google Drive + Google Drive allows you to sync your notes between devices using your own Google Drive account. + Logged into Google Firebase before? + Sign In to Google Drive + Signing In… + + Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. diff --git a/scarlet/build.gradle b/scarlet/build.gradle index b7968bb6..c7e9428d 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -62,6 +62,10 @@ dependencies { implementation "com.google.firebase:firebase-auth:16.1.0" implementation "com.google.firebase:firebase-database:16.0.6" + def litho_version = "0.21.0" + compileOnly "com.facebook.litho:litho-annotations:$litho_version" + kapt "com.facebook.litho:litho-processor:$litho_version" + implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation 'com.google.http-client:google-http-client-gson:1.26.0' implementation('com.google.api-client:google-api-client-android:1.26.0') { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt new file mode 100644 index 00000000..f359fdc8 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -0,0 +1,122 @@ +package com.bijoysingh.quicknote.drive + +import android.graphics.drawable.Drawable +import android.text.Layout +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.VerticalScroll +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity +import com.maubis.scarlet.base.support.ui.LithoCircleDrawable +import com.maubis.scarlet.base.support.ui.ThemeColorType + +@LayoutSpec +object GDriveRootViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop loggingIn: Boolean): Component { + val activity = context.androidContext as CreateNoteActivity + val buttonTitle = when { + loggingIn -> R.string.google_drive_page_logging_in_button + else -> R.string.google_drive_page_login_button + } + return Column.create(context) + .backgroundColor(CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(GDriveContentView.create(context))) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.google_drive_page_login_firebase_button) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(Row.create(context) + .backgroundRes(R.drawable.accent_rounded_bg) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.gdrive_icon) + .heightDip(36f) + ) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(GDriveRootView.onGoogleClickEvent(context))) + .build() + } + + @OnEvent(ClickEvent::class) + fun onGoogleClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + +@LayoutSpec +object GDriveContentViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext): Component { + return Column.create(context) + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.google_drive_page_login_title) + .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.google_drive_page_login_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(GDriveIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_action_lock) + .titleRes(R.string.google_drive_page_login_lock_details)) + .child(GDriveIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_action_lock) + .titleRes(R.string.google_drive_page_login_lock_details)) + .build() + } +} + +@LayoutSpec +object GDriveIconViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop(resType = ResType.COLOR) bgColor: Int, + @Prop(resType = ResType.DRAWABLE) icon: Drawable, + @Prop(resType = ResType.STRING) title: String): Component { + return Column.create(context) + .paddingDip(YogaEdge.HORIZONTAL, 32f) + .paddingDip(YogaEdge.VERTICAL, 24f) + .child(Image.create(context) + .drawable(icon) + .background(LithoCircleDrawable(bgColor)) + .paddingDip(YogaEdge.ALL, 12f) + .marginDip(YogaEdge.BOTTOM, 12f) + .heightDip(64f)) + .child(Text.create(context) + .text(title) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .textSizeRes(R.dimen.font_size_normal) + .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT)) + .build() + } +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index e04a7caa..5839d089 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -7,6 +7,9 @@ import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.LithoView import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -22,9 +25,7 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity -import kotlinx.android.synthetic.main.gdrive_login.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference @@ -54,12 +55,20 @@ var sGDriveFirstSyncImage: Boolean get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) +const val KEY_G_DRIVE_LAST_SYNC_DELTA_MS = 1000 * 60 +const val KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC = "g_drive_first_time_sync_last_sync" +var sGDriveLastSync: Long + get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, 0L) + set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, value) + class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { private val RC_SIGN_IN = 31244 private val RC_SIGN_IN_PERMISSIONS = 32443 lateinit var context: Context + lateinit var component: Component + lateinit var componentContext: ComponentContext lateinit var mGoogleApiClient: GoogleApiClient var loggingIn = AtomicBoolean(false) @@ -67,22 +76,13 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.gdrive_login) context = this - setupSignInButton() + componentContext = ComponentContext(context) + setButton(false) setupGoogleLogin() notifyThemeChange() } - private fun setupSignInButton() { - signInButton.setOnClickListener { - if (!loggingIn.get()) { - setButton(true) - signIn() - } - } - } - private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder( GoogleSignInOptions.DEFAULT_SIGN_IN) @@ -127,23 +127,20 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed private fun setButton(state: Boolean) { loggingIn.set(state) - when (loggingIn.get()) { - true -> { - signInButton.setBackgroundResource(R.drawable.login_button_disabled) - signInButtonTitle.setText(R.string.logging_into_app) - } - false -> { - signInButton.setBackgroundResource(R.drawable.login_button_active) - signInButtonTitle.setText(R.string.login_with_google) - } - } + component = GDriveRootView.create(componentContext) + .onClick { + if (!loggingIn.get()) { + setButton(true) + signIn() + } + } + .loggingIn(state) + .build() + setContentView(LithoView.create(componentContext, component)) } override fun notifyThemeChange() { setSystemTheme() - containerLayout.setBackgroundColor(getThemeColor()) - signInToGDrive.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - signInToGDriveDetails.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) } fun onLoginComplete(account: GoogleSignInAccount) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 2b7a1cd9..8d667b5f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -297,12 +297,18 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { onSyncCompleted() } - fun resync(onSyncCompleted: () -> Unit) { + @Synchronized + fun resync(force: Boolean, onSyncCompleted: () -> Unit) { if (!isValidController) { onSyncCompleted() return } + if (!force && sGDriveLastSync > getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { + return + } + sGDriveLastSync = getTrueCurrentTime() + GlobalScope.launch { resyncNotesSync {} resyncTagsSync {} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index f6716c6e..79cb89df 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -40,11 +40,11 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { openIfNeeded(activity) } - override fun resyncDrive(onSyncCompleted: () -> Unit) { + override fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) { if (gDrive === null) { onSyncCompleted() return } - gDrive?.resync(onSyncCompleted) + gDrive?.resync(force, onSyncCompleted) } } \ No newline at end of file diff --git a/scarlet/src/main/res/layout/gdrive_login.xml b/scarlet/src/main/res/layout/gdrive_login.xml deleted file mode 100644 index a5a43bc6..00000000 --- a/scarlet/src/main/res/layout/gdrive_login.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/scarlet/src/main/res/values/strings.xml b/scarlet/src/main/res/values/strings.xml index cc67a93d..ed57d863 100644 --- a/scarlet/src/main/res/values/strings.xml +++ b/scarlet/src/main/res/values/strings.xml @@ -25,10 +25,4 @@ I understand I am about to delete all my data - - - Connection Failed - Sync on Google Drive - Google Drive allows you to sync your notes between devices using your own Google Drive account. - \ No newline at end of file From ddffec14bd24cf83a2837d5bfc679b142c9b889d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 8 May 2019 21:00:25 +0100 Subject: [PATCH 012/134] Reducing the amount of network calls --- .../scarlet/base/support/utils/LogUtils.kt | 14 +++ .../java/com/bijoysingh/quicknote/Scarlet.kt | 2 + .../quicknote/database/GDriveUploadData.kt | 3 + .../quicknote/drive/GDriveLoginActivity.kt | 30 +----- .../quicknote/drive/GDriveRemoteDatabase.kt | 91 +++++++++++++++++-- .../quicknote/drive/GDriveServiceHelper.kt | 12 +++ .../firebase/support/ScarletAuthenticator.kt | 4 +- 7 files changed, 121 insertions(+), 35 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/utils/LogUtils.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/LogUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/LogUtils.kt new file mode 100644 index 00000000..bf514fba --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/LogUtils.kt @@ -0,0 +1,14 @@ +package com.maubis.scarlet.base.support.utils + +import android.util.Log +import com.maubis.scarlet.base.BuildConfig + +fun log(message: String) { + log("Scarlet", message) +} + +fun log(tag: String, description: String) { + if (BuildConfig.DEBUG) { + Log.d(tag, description) + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index 951abc1c..29e5f3e7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -3,6 +3,7 @@ package com.bijoysingh.quicknote import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase import com.bijoysingh.quicknote.firebase.FirebaseRemoteDatabase import com.bijoysingh.quicknote.scarlet.ScarletConfig +import com.github.bijoysingh.starter.prefs.Store import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.export.support.ExternalFolderSync @@ -21,5 +22,6 @@ class Scarlet : ApplicationBase() { companion object { var firebase: FirebaseRemoteDatabase? = null var gDrive: GDriveRemoteDatabase? = null + var gDriveConfig: Store? = null } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index d3e8bb0e..f58741c5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -46,6 +46,9 @@ interface GDriveUploadDataDao { @Delete fun delete(note: GDriveUploadData) + @Query("DELETE FROM gdrive_upload WHERE 1") + fun drop() + @Query("SELECT * FROM gdrive_upload WHERE uid = :uid LIMIT 1") fun getByID(uid: Int): GDriveUploadData? diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 5839d089..fadf6ff5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -5,11 +5,14 @@ import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView +import com.github.bijoysingh.starter.prefs.Store +import com.github.bijoysingh.starter.prefs.VersionedStore import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -25,6 +28,8 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.USER_PREFERENCES_STORE_NAME +import com.maubis.scarlet.base.config.USER_PREFERENCES_VERSION import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -36,31 +41,6 @@ import java.util.concurrent.atomic.AtomicBoolean // TODO: This is not ready... Recent changes in Drive API make this sh*t a little difficult and // inconclusive. I want to do this because it's safer than Firebase, but f*ck Google for // changing the API So much - -const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" - -var sGDriveFirstSyncNote: Boolean - get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) - set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) -var sGDriveFirstSyncTag: Boolean - get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) - set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) -var sGDriveFirstSyncFolder: Boolean - get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) - set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) -var sGDriveFirstSyncImage: Boolean - get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) - set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) - -const val KEY_G_DRIVE_LAST_SYNC_DELTA_MS = 1000 * 60 -const val KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC = "g_drive_first_time_sync_last_sync" -var sGDriveLastSync: Long - get() = CoreConfig.instance.store().get(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, 0L) - set(value) = CoreConfig.instance.store().put(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, value) - class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { private val RC_SIGN_IN = 31244 diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 8d667b5f..5d8c0d78 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,11 +1,14 @@ package com.bijoysingh.quicknote.drive import android.content.Context +import com.bijoysingh.quicknote.Scarlet +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.bijoysingh.quicknote.database.genGDriveUploadDatabase import com.bijoysingh.quicknote.firebase.data.* +import com.github.bijoysingh.starter.prefs.Store import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.config.CoreConfig @@ -27,6 +30,40 @@ const val FOLDER_NAME_DELETED_NOTES = "deleted_notes" const val FOLDER_NAME_DELETED_TAGS = "deleted_tags" const val FOLDER_NAME_DELETED_FOLDERS = "deleted_folders" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" +var sGDriveFirstSyncNote: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) ?: Unit +var sGDriveFirstSyncTag: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) ?: Unit +var sGDriveFirstSyncFolder: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) ?: Unit +var sGDriveFirstSyncImage: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) ?: Unit + +const val KEY_G_DRIVE_LAST_SYNC_DELTA_MS = 1000 * 60 +const val KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC = "g_drive_first_time_sync_last_sync" +var sGDriveLastSync: Long + get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, 0L) ?: 0L + set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, value) ?: Unit + +fun folderIdForFolderName(folderName: String, folderId: String = ""): String { + val key = "g_drive_folder_if_for_$folderName" + return when (folderId.isEmpty()) { + true -> gDriveConfig?.get(key, "") ?: "" + false -> { + gDriveConfig?.put(key, folderId) + folderId + } + } +} + class GDriveRemoteDatabase(val weakContext: WeakReference) { var gDriveDatabase: GDriveUploadDataDao? = null @@ -48,6 +85,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { isValidController = true driveHelper = helper gDriveDatabase = genGDriveUploadDatabase(context) + gDriveConfig = Store.get(context, "gdrive_config") notesSync = GDriveRemoteFolder(GDriveDataType.NOTE, gDriveDatabase!!, helper) { CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() @@ -61,10 +99,19 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { imageSync = GDriveRemoteImageFolder(GDriveDataType.IMAGE, gDriveDatabase!!, helper) GlobalScope.launch { - driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { - when { - (it === null) -> reset() - else -> onRootFolderLoaded(it) + val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) + when { + fuid.isNotBlank() -> onRootFolderLoaded(fuid) + else -> { + driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { + when { + (it === null) -> reset() + else -> { + folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER, it) + onRootFolderLoaded(it) + } + } + } } } } @@ -108,17 +155,31 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } fun createFolders(rootFolderId: String, expectedFolders: List) { - driveHelper?.getSubRootFolders(rootFolderId, expectedFolders)?.addOnCompleteListener { + val knownFolderIds = expectedFolders.filter { folderIdForFolderName(it).isNotEmpty() } + knownFolderIds.forEach { + GlobalScope.launch { initSubRootFolder(it, folderIdForFolderName(it)) } + } + + val unknownFolderIds = expectedFolders.filter { !knownFolderIds.contains(it) } + if (unknownFolderIds.isEmpty()) { + return + } + + driveHelper?.getSubRootFolders(rootFolderId, unknownFolderIds)?.addOnCompleteListener { val fileIds = it.result?.files ?: emptyList() val existingFiles = fileIds.map { it.name } fileIds.forEach { - GlobalScope.launch { initSubRootFolder(it.name, it.id) } + GlobalScope.launch { + folderIdForFolderName(it.name, it.id) + initSubRootFolder(it.name, it.id) + } } - expectedFolders.forEach { expectedFolder -> + unknownFolderIds.forEach { expectedFolder -> if (!existingFiles.contains(expectedFolder)) { driveHelper?.createFolder(rootFolderId, expectedFolder)?.addOnCompleteListener { fileIdTask -> val file = fileIdTask.result if (file !== null) { + folderIdForFolderName(expectedFolder, file.id) GlobalScope.launch { initSubRootFolder(expectedFolder, file.id) } } } @@ -136,6 +197,14 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { imageSync = null } + fun logout() { + GlobalScope.launch { + reset() + gDriveDatabase?.drop() + gDriveConfig?.clearSync() + } + } + private fun deleteEverything() { if (!isValidController) { return @@ -305,6 +374,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } if (!force && sGDriveLastSync > getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { + onSyncCompleted() return } sGDriveLastSync = getTrueCurrentTime() @@ -346,7 +416,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() existing.apply { uuid = itemUUID - type = GDriveDataType.NOTE.name + type = itemType.name lastUpdateTimestamp = gDriveUpdateTimestamp localStateDeleted = gDriveStateDeleted save(database) @@ -486,6 +556,11 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { val imageUUID = toImageUUID(data.uuid) if (imageUUID !== null) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + if (imageFile.exists()) { + remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + return + } + driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index d74b74a1..712d4012 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -9,6 +9,7 @@ import com.google.api.client.util.DateTime import com.google.api.services.drive.Drive import com.google.api.services.drive.model.File import com.google.api.services.drive.model.FileList +import com.maubis.scarlet.base.support.utils.log import java.io.BufferedReader import java.io.FileOutputStream import java.io.InputStreamReader @@ -52,6 +53,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { + log("GDrive", "createFileWithData($folderId, $name)") val contentToSave = if (content.isEmpty()) updateTime.toString() else content return execute(Callable { val metadata = File() @@ -65,6 +67,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { + log("GDrive", "createFileWithData($folderId, $name, ${file.absolutePath})") return execute(Callable { val metadata = File() .setParents(listOf(folderId)) @@ -77,6 +80,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun createFolder(parentUid: String, folderName: String): Task { + log("GDrive", "createFolder($parentUid, $folderName)") return execute(Callable { val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) @@ -90,6 +94,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun readFile(fileId: String): Task { + log("GDrive", "readFile($fileId)") return execute(Callable { mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> BufferedReader(InputStreamReader(`is`)).use { reader -> @@ -107,6 +112,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun readFile(fileId: String, destinationFile: java.io.File): Task { + log("GDrive", "readFile($fileId, ${destinationFile.absolutePath})") return execute(Callable { destinationFile.parentFile.mkdirs() val fileStream = FileOutputStream(destinationFile) @@ -116,6 +122,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { + log("GDrive", "saveFile($fileId, $name)") return execute(Callable { val metadata = File().setModifiedTime(DateTime(updateTime)).setName(name) val contentStream = ByteArrayContent.fromString("text/plain", content) @@ -124,6 +131,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { + log("GDrive", "getFilesInFolder($parentUid, $mimeType)") return execute(Callable { mDriveService.files().list() .setSpaces("drive") @@ -135,6 +143,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun getFolderQuery(parentUid: String, name: String): Task { + log("GDrive", "getFolderQuery($parentUid, $name)") val query = when { parentUid.isEmpty() -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name'" else -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name' and '$parentUid' in parents" @@ -148,6 +157,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun getSubRootFolders(parentUid: String, names: List): Task { + log("GDrive", "getSubRootFolders($parentUid, $names)") var nameQueryBuilder = "name = '${names[0]}'" names.subList(1, names.lastIndex + 1).forEach { nameQueryBuilder += " or name = '$it'" @@ -161,6 +171,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun removeFileOrFolder(fileUid: String): Task { + log("GDrive", "removeFileOrFolder($fileUid)") return execute(Callable { mDriveService.files().delete(fileUid) null @@ -168,6 +179,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } fun getOrCreateDirectory(parentUid: String, name: String, onFolderId: (String?) -> Unit) { + log("GDrive", "getOrCreateDirectory($parentUid, $name)") getFolderQuery(parentUid, name).addOnCompleteListener { getTask -> val fid = getTask.result?.files?.firstOrNull()?.id if (fid !== null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index 24193d0f..f34e328d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -68,8 +68,8 @@ class ScarletAuthenticator() : IAuthenticator { } override fun openLoginActivity(context: Context) = Runnable { - context.startActivity(Intent(context, GDriveLoginActivity::class.java)) - // context.startActivity(Intent(context, LoginActivity::class.java)) + // context.startActivity(Intent(context, GDriveLoginActivity::class.java)) + context.startActivity(Intent(context, LoginActivity::class.java)) } override fun openForgetMeActivity(context: Context) = Runnable { From 84894e28a3bc985f25253230331e7fef0103b71f Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 15 May 2019 23:16:28 +0100 Subject: [PATCH 013/134] [CrashFix] Fixing insta-crash on start due to class not found --- .../com/bijoysingh/quicknote/MaterialNotes.kt | 5 +-- .../com/maubis/scarlet/base/MainActivity.kt | 12 +++--- .../scarlet/base/config/ApplicationBase.kt | 2 +- .../maubis/scarlet/base/config/CoreConfig.kt | 2 +- .../base/core/folder/MaterialFolderActor.kt | 10 ++--- .../scarlet/base/core/format/FormatBuilder.kt | 1 - .../base/core/note/MaterialNoteActor.kt | 4 +- .../scarlet/base/core/note/NoteBuilder.kt | 2 +- .../scarlet/base/core/note/NoteExtensions.kt | 3 +- .../scarlet/base/core/note/NoteImage.kt | 14 +++---- .../base/core/note/NoteSortingUtils.kt | 2 - .../scarlet/base/core/tag/MaterialTagActor.kt | 9 ++-- .../scarlet/base/database/FoldersProvider.kt | 6 +-- .../scarlet/base/database/NotesProvider.kt | 5 +-- .../scarlet/base/database/TagsProvider.kt | 4 +- .../base/database/room/folder/FolderDao.java | 2 - .../export/activity/ImportNoteActivity.kt | 8 ++-- .../base/export/data/ExportableNote.kt | 6 +-- .../scarlet/base/export/data/ExportableTag.kt | 2 +- .../export/recycler/FileImportViewHolder.kt | 6 +-- .../base/export/remote/RemoteFolder.kt | 6 +-- .../sheet/BackupSettingsOptionsBottomSheet.kt | 4 +- .../export/sheet/ExportNotesBottomSheet.kt | 6 +-- .../sheet/ExternalFolderSyncBottomSheet.kt | 8 ++-- .../export/sheet/PermissionBottomSheet.kt | 4 +- .../base/export/support/ExternalFolderSync.kt | 20 ++++----- .../base/export/support/NoteExporter.kt | 18 ++++---- .../activity/OpenTextIntentOrFileActivity.kt | 18 ++++---- .../main/activity/WidgetConfigureActivity.kt | 7 ++-- .../main/recycler/InformationRecyclerItem.kt | 42 +++++++++---------- .../recycler/ToolbarMainRecyclerHolder.kt | 7 ++-- .../base/main/sheets/AlertBottomSheet.kt | 7 ++-- .../main/sheets/EnterPincodeBottomSheet.kt | 16 +++---- .../main/sheets/HomeNavigationBottomSheet.kt | 8 ++-- .../sheets/InstallProUpsellBottomSheet.kt | 6 +-- .../base/main/sheets/NoPincodeBottomSheet.kt | 8 ++-- .../base/main/sheets/WhatsNewBottomSheet.kt | 12 +++--- .../scarlet/base/main/utils/MainSnackbar.kt | 2 +- .../scarlet/base/note/NoteExtensions.kt | 20 ++++----- .../note/actions/NoteOptionsBottomSheet.kt | 16 +++---- .../note/actions/TextToSpeechBottomSheet.kt | 6 +-- .../note/activity/INoteOptionSheetActivity.kt | 2 +- .../creation/activity/CreateNoteActivity.kt | 5 ++- .../creation/activity/ViewNoteActivity.kt | 15 ++++--- .../sheet/EditorOptionsBottomSheet.kt | 18 ++++---- .../creation/sheet/FormatActionBottomSheet.kt | 1 - .../creation/sheet/MarkdownHelpBottomSheet.kt | 4 +- .../note/creation/specs/NoteViewTopBarSpec.kt | 2 - .../base/note/folder/FolderExtensions.kt | 14 +++---- .../base/note/folder/FolderRecyclerItem.kt | 2 +- .../sheet/CreateOrEditFolderBottomSheet.kt | 8 ++-- .../sheet/FolderChooseOptionsBottomSheet.kt | 6 +-- .../sheet/FolderOptionItemBottomSheetBase.kt | 4 +- .../SelectedFolderChooseOptionBottomSheet.kt | 11 ++--- .../formats/recycler/FormatImageViewHolder.kt | 2 - .../formats/recycler/FormatViewHolderBase.kt | 6 +-- .../recycler/NoteRecyclerViewHolderBase.kt | 3 +- .../base/note/reminders/ReminderJob.kt | 2 +- .../reminders/sheet/ReminderBottomSheet.kt | 8 ++-- .../selection/activity/SelectNotesActivity.kt | 2 +- .../activity/SelectableNotesActivityBase.kt | 12 +++--- .../scarlet/base/note/tag/TagExtensions.kt | 14 +++---- .../tag/sheet/CreateOrEditTagBottomSheet.kt | 8 ++-- .../SelectedTagChooseOptionsBottomSheet.kt | 4 +- .../tag/sheet/TagChooseOptionsBottomSheet.kt | 4 +- .../tag/sheet/TagOptionItemBottomSheetBase.kt | 4 +- .../tag/view/TagsAndColorPickerViewHolder.kt | 4 +- .../base/notification/NotificationHandler.kt | 4 +- .../notification/NotificationIntentService.kt | 8 ++-- .../base/service/FloatingNoteService.kt | 6 +-- .../sheet/AboutSettingsOptionsBottomSheet.kt | 4 +- .../base/settings/sheet/AboutUsBottomSheet.kt | 11 ++--- .../sheet/DeleteAndMoreOptionsBottomSheet.kt | 6 +-- .../settings/sheet/FontSizeBottomSheet.kt | 8 ++-- .../settings/sheet/LineCountBottomSheet.kt | 6 +-- .../sheet/NoteSettingsOptionsBottomSheet.kt | 6 +-- .../settings/sheet/OpenSourceBottomSheet.kt | 7 ++-- .../sheet/SecurityOptionsBottomSheet.kt | 24 +++++------ .../sheet/SettingsOptionsBottomSheet.kt | 12 +++--- .../sheet/SortingOptionsBottomSheet.kt | 6 +-- .../sheet/ThemeColorPickerBottomSheet.kt | 4 +- .../sheet/UISettingsOptionsBottomSheet.kt | 22 +++++----- .../base/support/database/HouseKeeper.kt | 1 - .../base/support/database/MigrationUtils.kt | 16 +++---- .../support/sheets/GridBottomSheetBase.kt | 4 +- .../base/support/sheets/LithoBottomSheet.kt | 7 ++-- .../sheets/LithoChooseOptionBottomSheet.kt | 3 +- .../support/sheets/LithoOptionBottomSheet.kt | 4 +- .../base/support/specs/BottomSheetBarSpec.kt | 5 ++- .../base/support/specs/CounterChooserSpec.kt | 3 +- .../base/support/specs/RoundIconSpec.kt | 1 - .../scarlet/base/support/specs/SpecUtils.kt | 6 +-- .../scarlet/base/support/ui/CircleDrawable.kt | 4 +- .../base/support/ui/LithoCircleDrawable.kt | 4 +- .../scarlet/base/support/ui/ThemeManager.kt | 4 +- .../scarlet/base/support/ui/ThemedActivity.kt | 8 ++-- .../support/ui/ThemedBottomSheetFragment.kt | 13 +++--- .../base/support/utils/AppVersionUtils.kt | 14 +++---- .../base/support/utils/FlavourUtils.kt | 6 +-- .../base/widget/AllNotesWidgetProvider.kt | 6 +-- .../scarlet/base/widget/NoteWidgetProvider.kt | 8 ++-- .../widget/sheet/WidgetOptionsBottomSheet.kt | 17 ++++---- .../java/com/bijoysingh/quicknote/Scarlet.kt | 9 ++-- .../quicknote/drive/GDriveActivitySpecs.kt | 13 +++--- .../quicknote/drive/GDriveLoginActivity.kt | 13 ++---- .../quicknote/drive/GDriveRemoteDatabase.kt | 14 +++---- .../quicknote/drive/GDriveRemoteFolder.kt | 2 - .../firebase/activity/DataPolicyActivity.kt | 17 ++++---- .../firebase/activity/ForgetMeActivity.kt | 11 +++-- .../firebase/activity/LoginActivity.kt | 12 +++--- .../firebase/support/RemoteConfigFetcher.kt | 16 +++---- .../firebase/support/ScarletAuthenticator.kt | 4 +- .../quicknote/scarlet/ScarletConfig.kt | 8 ++-- .../quicknote/scarlet/ScarletFolderActor.kt | 2 +- .../quicknote/scarlet/ScarletNoteActor.kt | 2 +- .../quicknote/scarlet/ScarletTagActor.kt | 2 +- 116 files changed, 440 insertions(+), 464 deletions(-) diff --git a/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt b/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt index d9d43e54..697f1147 100644 --- a/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt +++ b/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt @@ -1,7 +1,6 @@ package com.bijoysingh.quicknote import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.MaterialNoteConfig import com.maubis.scarlet.base.export.support.ExternalFolderSync @@ -9,8 +8,8 @@ class MaterialNotes : ApplicationBase() { override fun onCreate() { super.onCreate() - CoreConfig.instance = MaterialNoteConfig(this) - CoreConfig.instance.themeController().setup(this) + ApplicationBase.instance = MaterialNoteConfig(this) + ApplicationBase.instance.themeController().setup(this) ExternalFolderSync.setup(this) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 787d0772..6420dc3a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -13,7 +13,7 @@ import android.widget.GridLayout.VERTICAL import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag @@ -140,8 +140,8 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { val staggeredView = UISettingsOptionsBottomSheet.useGridView val isTablet = resources.getBoolean(R.bool.is_tablet) - val isMarkdownEnabled = CoreConfig.instance.store().get(KEY_MARKDOWN_ENABLED, true) - val isMarkdownHomeEnabled = CoreConfig.instance.store().get(KEY_MARKDOWN_HOME_ENABLED, true) + val isMarkdownEnabled = ApplicationBase.instance.store().get(KEY_MARKDOWN_ENABLED, true) + val isMarkdownHomeEnabled = ApplicationBase.instance.store().get(KEY_MARKDOWN_HOME_ENABLED, true) val adapterExtra = Bundle() adapterExtra.putBoolean(KEY_MARKDOWN_ENABLED, isMarkdownEnabled && isMarkdownHomeEnabled) adapterExtra.putInt(STORE_KEY_LINE_COUNT, sNoteItemLineCount) @@ -321,13 +321,13 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { override fun onResume() { super.onResume() - CoreConfig.instance.startListener(this) + ApplicationBase.instance.startListener(this) setupData() registerNoteReceiver() topSyncingLayout.visibility = View.VISIBLE GlobalScope.launch { - CoreConfig.instance.resyncDrive(false) { + ApplicationBase.instance.resyncDrive(false) { GlobalScope.launch(Dispatchers.Main) { topSyncingLayout.visibility = View.GONE } @@ -413,7 +413,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { override fun notifyThemeChange() { setSystemTheme() - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() containerLayoutMain.setBackgroundColor(getThemeColor()) val toolbarIconColor = theme.get(ThemeColorType.TOOLBAR_ICON) diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index c74dde17..73988546 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -4,7 +4,6 @@ import android.app.Application import com.evernote.android.job.JobManager import com.facebook.soloader.SoLoader import com.maubis.scarlet.base.core.note.NoteImage -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator @@ -18,6 +17,7 @@ abstract class ApplicationBase : Application() { companion object { lateinit var noteImagesFolder: NoteImage + lateinit var instance: CoreConfig var folderSync: FolderRemoteDatabase? = null } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index afdf710a..3c6d1b7e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -8,6 +8,7 @@ import com.github.ajalt.reprint.core.Reprint import com.github.bijoysingh.starter.prefs.Store import com.maubis.markdown.MarkdownConfig.Companion.config import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.core.folder.IFolderActor @@ -67,7 +68,6 @@ abstract class CoreConfig(context: Context) { abstract fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) companion object { - lateinit var instance: CoreConfig val notesDb get() = instance.notesDatabase() val tagsDb get() = instance.tagsDatabase() val foldersDb get() = instance.foldersDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt index 972a5a55..ab120899 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/folder/MaterialFolderActor.kt @@ -1,16 +1,16 @@ package com.maubis.scarlet.base.core.folder +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.export.data.ExportableFolder open class MaterialFolderActor(val folder: Folder) : IFolderActor { override fun offlineSave() { - val id = CoreConfig.instance.foldersDatabase().database().insertFolder(folder) + val id = ApplicationBase.instance.foldersDatabase().database().insertFolder(folder) folder.uid = if (folder.isUnsaved()) id.toInt() else folder.uid - CoreConfig.instance.foldersDatabase().notifyInsertFolder(folder) + ApplicationBase.instance.foldersDatabase().notifyInsertFolder(folder) } override fun onlineSave() { @@ -26,8 +26,8 @@ open class MaterialFolderActor(val folder: Folder) : IFolderActor { if (folder.isUnsaved()) { return } - CoreConfig.instance.foldersDatabase().database().delete(folder) - CoreConfig.instance.foldersDatabase().notifyDelete(folder) + ApplicationBase.instance.foldersDatabase().database().delete(folder) + ApplicationBase.instance.foldersDatabase().notifyDelete(folder) folder.uid = 0 } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt index f2e357e1..f7321436 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt @@ -1,7 +1,6 @@ package com.maubis.scarlet.base.core.format import com.maubis.markdown.segmenter.TextSegmenter -import com.maubis.scarlet.base.note.toFormat import com.maubis.scarlet.base.note.toRawFormat import org.json.JSONArray import org.json.JSONException diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index a45ba133..bbe3e918 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -8,9 +8,9 @@ import android.support.v7.app.AppCompatActivity import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.database.room.note.Note @@ -108,7 +108,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { notifyAllChanged(context) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? notificationManager?.cancel(note.uid) - CoreConfig.instance.imageCache().deleteNote(note.uuid) + ApplicationBase.instance.imageCache().deleteNote(note.uuid) } protected fun onNoteUpdated(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt index b0d8d563..2cbcfff8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt @@ -3,10 +3,10 @@ package com.maubis.scarlet.base.core.note import com.github.bijoysingh.starter.util.RandomHelper import com.github.bijoysingh.starter.util.TextUtils import com.google.gson.Gson -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.database.room.note.Note import java.util.* fun generateUUID() = UUID.randomUUID().toString() diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt index bc706808..2748f5e8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt @@ -2,10 +2,9 @@ package com.maubis.scarlet.base.core.note import com.github.bijoysingh.starter.util.TextUtils import com.google.gson.Gson -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder -import com.maubis.scarlet.base.note.saveWithoutSync +import com.maubis.scarlet.base.database.room.note.Note import java.util.* fun Note.isUnsaved(): Boolean { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt index 6bf080e7..05729fc6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt @@ -4,7 +4,7 @@ import android.content.Context import android.view.View import android.widget.ImageView import com.github.bijoysingh.starter.util.RandomHelper -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType @@ -66,7 +66,7 @@ class NoteImage(context: Context) { return@launch } - val bitmap = CoreConfig.instance.imageCache().loadFromCache(file) + val bitmap = ApplicationBase.instance.imageCache().loadFromCache(file) if (bitmap === null) { deleteIfExist(file) GlobalScope.launch(Dispatchers.Main) { @@ -87,8 +87,8 @@ class NoteImage(context: Context) { image: ImageView, callback: ImageLoadCallback? = null) { GlobalScope.launch { - val thumbnailFile = CoreConfig.instance.imageCache().thumbnailFile(noteUUID, imageUuid) - val persistentFile = CoreConfig.instance.imageCache().persistentFile(noteUUID, imageUuid) + val thumbnailFile = ApplicationBase.instance.imageCache().thumbnailFile(noteUUID, imageUuid) + val persistentFile = ApplicationBase.instance.imageCache().persistentFile(noteUUID, imageUuid) if (!persistentFile.exists()) { GlobalScope.launch(Dispatchers.Main) { @@ -99,7 +99,7 @@ class NoteImage(context: Context) { } if (thumbnailFile.exists()) { - val bitmap = CoreConfig.instance.imageCache().loadFromCache(thumbnailFile) + val bitmap = ApplicationBase.instance.imageCache().loadFromCache(thumbnailFile) if (bitmap === null) { deleteIfExist(thumbnailFile) GlobalScope.launch(Dispatchers.Main) { @@ -116,7 +116,7 @@ class NoteImage(context: Context) { return@launch } - val persistentBitmap = CoreConfig.instance.imageCache().loadFromCache(persistentFile) + val persistentBitmap = ApplicationBase.instance.imageCache().loadFromCache(persistentFile) if (persistentBitmap === null) { deleteIfExist(persistentFile) GlobalScope.launch(Dispatchers.Main) { @@ -126,7 +126,7 @@ class NoteImage(context: Context) { return@launch } - val compressedBitmap = CoreConfig.instance.imageCache().saveThumbnail(thumbnailFile, persistentBitmap) + val compressedBitmap = ApplicationBase.instance.imageCache().saveThumbnail(thumbnailFile, persistentBitmap) GlobalScope.launch(Dispatchers.Main) { image.visibility = View.VISIBLE image.setImageBitmap(compressedBitmap) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt index 1113cc22..dab38cdd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt @@ -2,8 +2,6 @@ package com.maubis.scarlet.base.core.note import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.getAlphabets -import com.maubis.scarlet.base.note.getFullText -import com.maubis.scarlet.base.note.getText enum class SortingTechnique() { LAST_MODIFIED, diff --git a/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt index 90c60cfd..3d80d88a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/tag/MaterialTagActor.kt @@ -1,15 +1,14 @@ package com.maubis.scarlet.base.core.tag import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.data.ExportableTag open class MaterialTagActor(val tag: Tag) : ITagActor { override fun offlineSave() { - val id = CoreConfig.instance.tagsDatabase().database().insertTag(tag) + val id = ApplicationBase.instance.tagsDatabase().database().insertTag(tag) tag.uid = if (tag.isUnsaved()) id.toInt() else tag.uid - CoreConfig.instance.tagsDatabase().notifyInsertTag(tag) + ApplicationBase.instance.tagsDatabase().notifyInsertTag(tag) } override fun onlineSave() { @@ -25,8 +24,8 @@ open class MaterialTagActor(val tag: Tag) : ITagActor { if (tag.isUnsaved()) { return } - CoreConfig.instance.tagsDatabase().database().delete(tag) - CoreConfig.instance.tagsDatabase().notifyDelete(tag) + ApplicationBase.instance.tagsDatabase().database().delete(tag) + ApplicationBase.instance.tagsDatabase().notifyDelete(tag) tag.uid = 0 } diff --git a/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt index 5a2d15c3..99305cf2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt @@ -1,10 +1,8 @@ package com.maubis.scarlet.base.database -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.folder.FolderDao -import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.database.room.tag.TagDao import java.util.concurrent.ConcurrentHashMap class FoldersProvider { @@ -72,6 +70,6 @@ class FoldersProvider { } fun database(): FolderDao { - return CoreConfig.instance.database().folders() + return ApplicationBase.instance.database().folders() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt index c19296ed..459f1af8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/NotesProvider.kt @@ -1,8 +1,7 @@ package com.maubis.scarlet.base.database -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.INoteContainer -import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.note.NoteDao import com.maubis.scarlet.base.note.applySanityChecks @@ -112,6 +111,6 @@ class NotesProvider { } fun database(): NoteDao { - return CoreConfig.instance.database().notes() + return ApplicationBase.instance.database().notes() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt index 4a3411dd..346773ac 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt @@ -1,6 +1,6 @@ package com.maubis.scarlet.base.database -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.database.room.tag.TagDao import java.util.concurrent.ConcurrentHashMap @@ -70,6 +70,6 @@ class TagsProvider { } fun database(): TagDao { - return CoreConfig.instance.database().tags() + return ApplicationBase.instance.database().tags() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/database/room/folder/FolderDao.java b/base/src/main/java/com/maubis/scarlet/base/database/room/folder/FolderDao.java index 0052c6d6..9c620dae 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/room/folder/FolderDao.java +++ b/base/src/main/java/com/maubis/scarlet/base/database/room/folder/FolderDao.java @@ -6,8 +6,6 @@ import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; -import com.maubis.scarlet.base.database.room.note.Note; - import java.util.List; @Dao diff --git a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt index 00fc8f9b..06030bd9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt @@ -9,14 +9,14 @@ import android.widget.TextView import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.recycler.FileRecyclerItem import com.maubis.scarlet.base.export.support.NoteImporter import com.maubis.scarlet.base.note.recycler.NoteAppAdapter -import com.maubis.scarlet.base.support.utils.bind import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.bind import java.io.File import java.io.FileReader @@ -95,13 +95,13 @@ class ImportNoteActivity : ThemedActivity() { } override fun notifyThemeChange() { - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() background.setBackgroundColor(theme.get(ThemeColorType.BACKGROUND)) backButton.setColorFilter(theme.get(ThemeColorType.TOOLBAR_ICON)) pageTitle.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setBackgroundResource( - if (CoreConfig.instance.themeController().isNightTheme()) R.drawable.light_circular_border_bg + if (ApplicationBase.instance.themeController().isNightTheme()) R.drawable.light_circular_border_bg else R.drawable.dark_circular_border_bg) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt index 08acf717..8584b0cf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt @@ -1,14 +1,14 @@ package com.maubis.scarlet.base.export.data import android.content.Context -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.INoteContainer import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.generateUUID +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.note.tag.save -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import org.json.JSONArray import org.json.JSONObject import java.io.Serializable diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt index 6e0ad4fe..3893ff5a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt @@ -1,9 +1,9 @@ package com.maubis.scarlet.base.export.data -import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.core.note.generateUUID import com.maubis.scarlet.base.core.tag.ITagContainer import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.tag.Tag import org.json.JSONObject import java.io.Serializable diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt index b0398925..e75bb1a1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt @@ -10,7 +10,7 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -25,7 +25,7 @@ class FileImportViewHolder(context: Context, root: View) private val fileSize: TextView = findViewById(R.id.file_size) init { - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() fileName.setTextColor(theme.get(ThemeColorType.SECONDARY_TEXT)) filePath.setTextColor(theme.get(ThemeColorType.HINT_TEXT)) fileDate.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) @@ -43,7 +43,7 @@ class FileImportViewHolder(context: Context, root: View) (context as ImportNoteActivity).select(item) } root.setBackgroundColor( - if (item.selected) CoreConfig.instance.themeController().get( + if (item.selected) ApplicationBase.instance.themeController().get( context, R.color.material_grey_100, R.color.dark_hint_text) else Color.TRANSPARENT) } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt index 29cd1ceb..dd364f25 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.export.remote import com.github.bijoysingh.starter.util.FileManager import com.google.gson.Gson -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.support.KEY_EXTERNAL_FOLDER_SYNC_LAST_SCAN import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -22,7 +22,7 @@ class RemoteFolder(val folder: File, val deletedUuids = HashSet() val lastScanKey = "${KEY_EXTERNAL_FOLDER_SYNC_LAST_SCAN}_${folder.name}" - var lastScan = CoreConfig.instance.store().get(lastScanKey, 0L) + var lastScan = ApplicationBase.instance.store().get(lastScanKey, 0L) init { GlobalScope.launch(Dispatchers.IO) { @@ -50,7 +50,7 @@ class RemoteFolder(val folder: File, } onInitComplete() - CoreConfig.instance.store().put(lastScanKey, System.currentTimeMillis()) + ApplicationBase.instance.store().put(lastScanKey, System.currentTimeMillis()) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt index ad39a96d..422b4121 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt @@ -5,7 +5,7 @@ import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet @@ -30,7 +30,7 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { IntentUtils.openAppPlayStore(context) dismiss() }, - visible = CoreConfig.instance.appFlavor() == Flavor.NONE + visible = ApplicationBase.instance.appFlavor() == Flavor.NONE )) options.add(LithoOptionsItem( title = R.string.home_option_export, diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt index 45b69abb..ecd506d3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt @@ -12,7 +12,7 @@ import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.support.* import com.maubis.scarlet.base.support.sheets.* import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -25,7 +25,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch val NOTES_EXPORT_FOLDER - get() = when (CoreConfig.instance.appFlavor()) { + get() = when (ApplicationBase.instance.appFlavor()) { Flavor.NONE -> "MaterialNotes" Flavor.LITE -> "Scarlet" Flavor.PRO -> "ScarletPro" @@ -52,7 +52,7 @@ class ExportNotesBottomSheet : LithoBottomSheet() { .text(filenameRender) .typeface(Typeface.MONOSPACE) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) getOptions(componentContext).forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index 747e429f..39067d79 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -9,7 +9,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.export.support.ExternalFolderSync import com.maubis.scarlet.base.export.support.sExternalFolderSync @@ -41,21 +41,21 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.import_export_layout_folder_sync_description) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .typeface(FONT_MONSERRAT) .textRes(R.string.import_export_layout_folder_sync_folder) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .text(sFolderSyncPath) .typeface(Typeface.MONOSPACE) .paddingDip(YogaEdge.HORIZONTAL, 20f) .paddingDip(YogaEdge.VERTICAL, 8f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) getOptions(componentContext).forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt index 23da9de3..8c30f5fb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt @@ -7,7 +7,7 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -29,7 +29,7 @@ class PermissionBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.permission_layout_give_permission_details) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.permission_layout_give_permission_ok) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index 5f24c9b4..c2e07016 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -7,8 +7,8 @@ import android.os.Build import android.support.v4.content.ContextCompat import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.export.data.ExportableFolder import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.export.data.ExportableTag @@ -26,16 +26,16 @@ const val KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED = "external_folder_sync_backup_ const val KEY_EXTERNAL_FOLDER_SYNC_PATH = "external_folder_sync_path" var sExternalFolderSync: Boolean - get() = CoreConfig.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, false) - set(value) = CoreConfig.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, value) + get() = ApplicationBase.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, false) + set(value) = ApplicationBase.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, value) var sFolderSyncPath: String - get() = CoreConfig.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_PATH, "$NOTES_EXPORT_FOLDER/Sync/") - set(value) = CoreConfig.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_PATH, value) + get() = ApplicationBase.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_PATH, "$NOTES_EXPORT_FOLDER/Sync/") + set(value) = ApplicationBase.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_PATH, value) var sFolderSyncBackupLocked: Boolean - get() = CoreConfig.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, true) - set(value) = CoreConfig.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, value) + get() = ApplicationBase.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, true) + set(value) = ApplicationBase.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, value) object ExternalFolderSync { @@ -63,17 +63,17 @@ object ExternalFolderSync { fun loadFirstTime() { folderSync?.init( { - CoreConfig.instance.notesDatabase().getAll().forEach { + ApplicationBase.instance.notesDatabase().getAll().forEach { folderSync?.insert(ExportableNote(it)) } }, { - CoreConfig.instance.tagsDatabase().getAll().forEach { + ApplicationBase.instance.tagsDatabase().getAll().forEach { folderSync?.insert(ExportableTag(it)) } }, { - CoreConfig.instance.foldersDatabase().getAll().forEach { + ApplicationBase.instance.foldersDatabase().getAll().forEach { folderSync?.insert(ExportableFolder(it)) } }) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt index c548cbaa..f944b80a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt @@ -5,7 +5,7 @@ import android.os.Environment import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.starter.util.FileManager import com.google.gson.Gson -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -31,18 +31,18 @@ const val AUTO_BACKUP_INTERVAL_MS = 1000 * 60 * 60 * 6 // 6 hours update const val STORE_KEY_BACKUP_MARKDOWN = "KEY_BACKUP_MARKDOWN" var sBackupMarkdown: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_BACKUP_MARKDOWN, false) - set(value) = CoreConfig.instance.store().put(STORE_KEY_BACKUP_MARKDOWN, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_BACKUP_MARKDOWN, false) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_BACKUP_MARKDOWN, value) const val STORE_KEY_BACKUP_LOCKED = "KEY_BACKUP_LOCKED" var sBackupLockedNotes: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_BACKUP_LOCKED, true) - set(value) = CoreConfig.instance.store().put(STORE_KEY_BACKUP_LOCKED, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_BACKUP_LOCKED, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_BACKUP_LOCKED, value) const val STORE_KEY_AUTO_BACKUP_MODE = "KEY_AUTO_BACKUP_MODE" var sAutoBackupMode: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_AUTO_BACKUP_MODE, false) - set(value) = CoreConfig.instance.store().put(STORE_KEY_AUTO_BACKUP_MODE, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_AUTO_BACKUP_MODE, false) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_AUTO_BACKUP_MODE, value) class NoteExporter() { @@ -77,7 +77,7 @@ class NoteExporter() { if (!sAutoBackupMode) { return@execute } - val lastBackup = CoreConfig.instance.store().get(KEY_AUTO_BACKUP_LAST_TIMESTAMP, 0L) + val lastBackup = ApplicationBase.instance.store().get(KEY_AUTO_BACKUP_LAST_TIMESTAMP, 0L) val lastTimestamp = notesDb.getLastTimestamp() if (lastBackup + AUTO_BACKUP_INTERVAL_MS >= lastTimestamp) { return@execute @@ -88,7 +88,7 @@ class NoteExporter() { return@execute } saveToFile(exportFile, getExportContent()) - CoreConfig.instance.store().put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) + ApplicationBase.instance.store().put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 17f3940d..70e23a12 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -14,21 +14,19 @@ import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.util.TextUtils import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.markdown.Markdown +import com.maubis.markdown.spannable.clearMarkdownSpans import com.maubis.markdown.spannable.setFormats import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.NoteBuilder +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.support.NoteImporter import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.save -import com.maubis.scarlet.base.support.utils.bind import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.bind import java.io.InputStreamReader -import android.support.annotation.NonNull -import android.text.style.* -import com.maubis.markdown.spannable.clearMarkdownSpans const val KEEP_PACKAGE = "com.google.android.keep" @@ -91,7 +89,7 @@ class OpenTextIntentOrFileActivity : ThemedActivity() { override fun onResume() { super.onResume() - CoreConfig.instance.startListener(this) + ApplicationBase.instance.startListener(this) } private fun setView() { @@ -184,15 +182,15 @@ class OpenTextIntentOrFileActivity : ThemedActivity() { val containerLayout = findViewById(R.id.container_layout); containerLayout.setBackgroundColor(getThemeColor()); - val toolbarIconColor = CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_ICON); + val toolbarIconColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON); backButton.setColorFilter(toolbarIconColor) - val textColor = CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val textColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) filename.setTextColor(textColor) title.setTextColor(textColor) content.setTextColor(textColor) - val actionColor = CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_ICON) + val actionColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON) actionDone.setImageTint(actionColor) actionDone.setTextColor(actionColor) } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt index 91da6a31..66fd897f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt @@ -11,9 +11,8 @@ import android.os.Bundle import android.support.v4.content.ContextCompat import android.widget.RemoteViews import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.widget.Widget import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity @@ -55,7 +54,7 @@ class WidgetConfigureActivity : SelectableNotesActivityBase(), INoteSelectorActi override fun onNoteClicked(note: Note) { val widget = Widget(appWidgetId, note.uuid) - CoreConfig.instance.database().widgets().insert(widget) + ApplicationBase.instance.database().widgets().insert(widget) createWidget(widget) } @@ -104,7 +103,7 @@ class WidgetConfigureActivity : SelectableNotesActivityBase(), INoteSelectorActi val application: Application = context.applicationContext as Application val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( ComponentName(application, NoteWidgetProvider::class.java)) - val widgets = CoreConfig.instance.database().widgets().getByNote(note.uuid) + val widgets = ApplicationBase.instance.database().widgets().getByNote(note.uuid) val widgetIds = ArrayList() for (widget in widgets) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt index 70f912c4..6e1fb426 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt @@ -6,7 +6,7 @@ import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.sheet.BackupSettingsOptionsBottomSheet import com.maubis.scarlet.base.export.support.NoteExporter import com.maubis.scarlet.base.main.activity.INTENT_KEY_DIRECT_NOTES_TRANSFER @@ -38,7 +38,7 @@ class InformationRecyclerItem(val icon: Int, val title: Int, val source: Int, va fun probability(probability: Float): Boolean = Random().nextFloat() <= probability fun shouldShowAppUpdateInformationItem(): Boolean { - return !CoreConfig.instance.remoteConfigFetcher().isLatestVersion() + return !ApplicationBase.instance.remoteConfigFetcher().isLatestVersion() } fun getAppUpdateInformationItem(context: Context): InformationRecyclerItem { @@ -51,7 +51,7 @@ fun getAppUpdateInformationItem(context: Context): InformationRecyclerItem { fun shouldShowReviewInformationItem(): Boolean { return probability(0.01f) - && !CoreConfig.instance.store().get(KEY_INFO_RATE_AND_REVIEW, false) + && !ApplicationBase.instance.store().get(KEY_INFO_RATE_AND_REVIEW, false) } fun getReviewInformationItem(context: Context): InformationRecyclerItem { @@ -60,14 +60,14 @@ fun getReviewInformationItem(context: Context): InformationRecyclerItem { R.string.home_option_rate_and_review, R.string.home_option_rate_and_review_subtitle ) { - CoreConfig.instance.store().put(KEY_INFO_RATE_AND_REVIEW, true) + ApplicationBase.instance.store().put(KEY_INFO_RATE_AND_REVIEW, true) IntentUtils.openAppPlayStore(context) } } fun shouldShowThemeInformationItem(): Boolean { return probability(0.01f) - && !CoreConfig.instance.store().get(KEY_THEME_OPTIONS, false) + && !ApplicationBase.instance.store().get(KEY_THEME_OPTIONS, false) } fun getThemeInformationItem(activity: MainActivity): InformationRecyclerItem { @@ -76,14 +76,14 @@ fun getThemeInformationItem(activity: MainActivity): InformationRecyclerItem { R.string.home_option_ui_experience, R.string.home_option_ui_experience_subtitle ) { - CoreConfig.instance.store().put(KEY_THEME_OPTIONS, true) + ApplicationBase.instance.store().put(KEY_THEME_OPTIONS, true) UISettingsOptionsBottomSheet.openSheet(activity) } } fun shouldShowBackupInformationItem(): Boolean { return probability(0.01f) - && !CoreConfig.instance.store().get(KEY_BACKUP_OPTIONS, false) + && !ApplicationBase.instance.store().get(KEY_BACKUP_OPTIONS, false) } fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { @@ -92,7 +92,7 @@ fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { R.string.home_option_backup_options, R.string.home_option_backup_options_subtitle ) { - CoreConfig.instance.store().put(KEY_BACKUP_OPTIONS, true) + ApplicationBase.instance.store().put(KEY_BACKUP_OPTIONS, true) openSheet(activity, BackupSettingsOptionsBottomSheet()) } } @@ -100,8 +100,8 @@ fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { fun shouldShowInstallProInformationItem(): Boolean { return probability(0.01f) - && CoreConfig.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT - && CoreConfig.instance.appFlavor() != Flavor.PRO + && ApplicationBase.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT + && ApplicationBase.instance.appFlavor() != Flavor.PRO } fun getInstallProInformationItem(context: Context): InformationRecyclerItem { @@ -116,16 +116,16 @@ fun getInstallProInformationItem(context: Context): InformationRecyclerItem { } fun shouldShowSignInformationItem(): Boolean { - if (CoreConfig.instance.authenticator().isLoggedIn() - || CoreConfig.instance.appFlavor() == Flavor.NONE) { + if (ApplicationBase.instance.authenticator().isLoggedIn() + || ApplicationBase.instance.appFlavor() == Flavor.NONE) { return false } - if (CoreConfig.instance.store().get(KEY_FORCE_SHOW_SIGN_IN, false)) { - CoreConfig.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, false) + if (ApplicationBase.instance.store().get(KEY_FORCE_SHOW_SIGN_IN, false)) { + ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, false) return true } return probability(0.01f) - && !CoreConfig.instance.store().get(KEY_INFO_SIGN_IN, false) + && !ApplicationBase.instance.store().get(KEY_INFO_SIGN_IN, false) } fun getSignInInformationItem(context: Context): InformationRecyclerItem { @@ -134,21 +134,21 @@ fun getSignInInformationItem(context: Context): InformationRecyclerItem { R.string.home_option_login_with_app, R.string.home_option_login_with_app_subtitle ) { - CoreConfig.instance.authenticator().openLoginActivity(context)?.run() + ApplicationBase.instance.authenticator().openLoginActivity(context)?.run() notifyProUpsellShown() } } fun notifyProUpsellShown() { - val proUpsellCount = CoreConfig.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) - CoreConfig.instance.store().put(KEY_INFO_INSTALL_PRO_v2, proUpsellCount + 1) + val proUpsellCount = ApplicationBase.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) + ApplicationBase.instance.store().put(KEY_INFO_INSTALL_PRO_v2, proUpsellCount + 1) } fun shouldShowMigrateToProAppInformationItem(context: Context): Boolean { - return CoreConfig.instance.appFlavor() == Flavor.LITE + return ApplicationBase.instance.appFlavor() == Flavor.LITE && FlavourUtils.hasProAppInstalled(context) - && !CoreConfig.instance.store().get(KEY_MIGRATE_TO_PRO_SUCCESS, false) + && !ApplicationBase.instance.store().get(KEY_MIGRATE_TO_PRO_SUCCESS, false) } fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem { @@ -165,7 +165,7 @@ fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem .setPackage(FlavourUtils.PRO_APP_PACKAGE_NAME) try { context.startActivity(intent) - CoreConfig.instance.store().put(KEY_MIGRATE_TO_PRO_SUCCESS, true) + ApplicationBase.instance.store().put(KEY_MIGRATE_TO_PRO_SUCCESS, true) } catch (e: Exception) { ToastHelper.show(context, "Failed transferring notes to Scarlet Pro") } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index a506c13c..01bd9362 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -10,8 +10,7 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.export.sheet.BackupSettingsOptionsBottomSheet +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.settings.sheet.DeleteAndMoreOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -36,10 +35,10 @@ class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerView SettingsOptionsBottomSheet.openSheet((context as MainActivity)) } - val titleColor = CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val titleColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) toolbarTitle.setTextColor(titleColor) - val toolbarIconColor = CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val toolbarIconColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) toolbarIconSearch.setColorFilter(toolbarIconColor) toolbarIconSettings.setColorFilter(toolbarIconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt index c8414d27..ca42c849 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt @@ -8,6 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.note.NoteState @@ -60,7 +61,7 @@ class AlertBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(config.description) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(config.positiveText) .onPrimaryClick { @@ -105,8 +106,8 @@ fun openDeleteFormatDialog(activity: ViewAdvancedNoteActivity, format: Format) { const val STORE_KEY_IMAGE_SYNC_NOTICE = "IMAGE_SYNC_NOTICE" var sImageSyncNoticeShown: Int - get() = CoreConfig.instance.store().get(STORE_KEY_IMAGE_SYNC_NOTICE, 0) - set(value) = CoreConfig.instance.store().put(STORE_KEY_IMAGE_SYNC_NOTICE, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_IMAGE_SYNC_NOTICE, 0) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_IMAGE_SYNC_NOTICE, value) fun openImageNotSynced(activity: ThemedActivity) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt index cb6dee59..a12a75fe 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt @@ -16,7 +16,7 @@ import com.github.ajalt.reprint.core.Reprint import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -49,10 +49,10 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { val fingerprint = dialog.findViewById(R.id.fingerprint) val removeBtn = dialog.findViewById(R.id.action_remove_button) - title.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterPin.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + enterPin.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - val hintColor = CoreConfig.instance.themeController().get(ThemeColorType.HINT_TEXT) + val hintColor = ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT) enterPin.setHintTextColor(hintColor) pinLength.setTextColor(hintColor) @@ -176,7 +176,7 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { override fun isRemoveButtonEnabled(): Boolean = true override fun onRemoveButtonClick() { - CoreConfig.instance.store().put(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, "") + ApplicationBase.instance.store().put(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, "") sNoPinSetupNoticeShown = false listener.onSuccess() @@ -185,7 +185,7 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { } override fun onPasswordRequested(password: String) { - CoreConfig.instance.store().put(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, password) + ApplicationBase.instance.store().put(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, password) listener.onSuccess() } @@ -239,11 +239,11 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { override fun isFingerprintEnabled(): Boolean { return Reprint.hasFingerprintRegistered() && - CoreConfig.instance.store().get(SecurityOptionsBottomSheet.KEY_FINGERPRINT_ENABLED, true) + ApplicationBase.instance.store().get(SecurityOptionsBottomSheet.KEY_FINGERPRINT_ENABLED, true) } override fun onPasswordRequested(password: String) { - val currentPassword = CoreConfig.instance.store().get(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, "") + val currentPassword = ApplicationBase.instance.store().get(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, "") if (currentPassword != "" && currentPassword == password) { listener.onSuccess() } else if (listener is PincodeSuccessListener) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt index 37bd8fb5..60eb48d8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt @@ -9,7 +9,7 @@ import com.github.bijoysingh.starter.util.LocaleManager import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.tag.TagBuilder @@ -115,7 +115,7 @@ class HomeNavigationBottomSheet : GridBottomSheetBase() { val titleView = dialog.findViewById(R.id.tag_options_title) titleView.setTextColor( - CoreConfig.instance.themeController().get(themedContext(), + ApplicationBase.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.SECONDARY_TEXT)) val layout = dialog.findViewById(R.id.options_container) @@ -134,7 +134,7 @@ class HomeNavigationBottomSheet : GridBottomSheetBase() { contentView.icon.setImageResource(option.getIcon()) contentView.action.setImageResource(option.getEditIcon()); - contentView.action.setColorFilter(CoreConfig.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.HINT_TEXT)); + contentView.action.setColorFilter(ApplicationBase.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.HINT_TEXT)); contentView.action.setOnClickListener(option.editListener) if (option.usages > 0) { @@ -172,7 +172,7 @@ class HomeNavigationBottomSheet : GridBottomSheetBase() { } private fun setAddTagOption(dialog: Dialog) { - val hintTextColor = CoreConfig.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.HINT_TEXT) + val hintTextColor = ApplicationBase.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.HINT_TEXT) val newTagButton = dialog.findViewById(R.id.new_tag_button) newTagButton.setTextColor(hintTextColor) newTagButton.setImageTint(hintTextColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index 3d3f4c0f..3d3f4986 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -8,7 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -31,12 +31,12 @@ class InstallProUpsellBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.why_install_pro) .typeface(FONT_MONSERRAT) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.why_install_pro_details) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.install_pro_app) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt index 0a0d34c1..6ba53b23 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt @@ -7,7 +7,7 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -17,8 +17,8 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity const val STORE_KEY_NO_PIN_ASK = "KEY_NO_PIN_ASK" var sNoPinSetupNoticeShown: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_NO_PIN_ASK, false) - set(value) = CoreConfig.instance.store().put(STORE_KEY_NO_PIN_ASK, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_NO_PIN_ASK, false) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_NO_PIN_ASK, value) class NoPincodeBottomSheet : LithoBottomSheet() { var onSuccess: () -> Unit = {} @@ -36,7 +36,7 @@ class NoPincodeBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.no_pincode_sheet_details) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.no_pincode_sheet_set_up) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 5694d30a..eb233beb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -10,7 +10,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -31,29 +31,29 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(WHATS_NEW_DETAILS_SUBTITLE) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .text(WHATS_NEW_DETAILS_NEW_FEATURES_TITLE) .typeface(FONT_MONSERRAT) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(Markdown.render(WHATS_NEW_DETAILS_NEW_FEATURES_MD, true)) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .text(WHATS_NEW_DETAILS_LAST_RELEASE_TITLE) .typeface(FONT_MONSERRAT) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(Markdown.render(WHATS_NEW_DETAILS_LAST_RELEASE_MD, true)) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.import_export_layout_exporting_done) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/utils/MainSnackbar.kt b/base/src/main/java/com/maubis/scarlet/base/main/utils/MainSnackbar.kt index dd72dd54..27143d35 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/utils/MainSnackbar.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/utils/MainSnackbar.kt @@ -7,10 +7,10 @@ import android.view.View.VISIBLE import android.widget.LinearLayout import android.widget.TextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getNoteState +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.save class MainSnackbar(val layout: LinearLayout, val alwaysRunnable: () -> Unit) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 0bab87c8..63ba5e84 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -6,7 +6,7 @@ import com.github.bijoysingh.starter.util.DateFormatter import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.segmenter.TextSegmenter -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType @@ -266,11 +266,11 @@ fun Note.openEdit(context: Context) { fun Note.share(context: Context) { - CoreConfig.instance.noteActions(this).share(context) + ApplicationBase.instance.noteActions(this).share(context) } fun Note.copy(context: Context) { - CoreConfig.instance.noteActions(this).copy(context) + ApplicationBase.instance.noteActions(this).copy(context) } /************************************************************************************** @@ -293,15 +293,15 @@ fun Note.save(context: Context) { saveWithoutSync(context) return } - CoreConfig.instance.noteActions(this).save(context) + ApplicationBase.instance.noteActions(this).save(context) } fun Note.saveWithoutSync(context: Context) { - CoreConfig.instance.noteActions(this).offlineSave(context) + ApplicationBase.instance.noteActions(this).offlineSave(context) } fun Note.saveToSync(context: Context) { - CoreConfig.instance.noteActions(this).onlineSave(context) + ApplicationBase.instance.noteActions(this).onlineSave(context) } fun Note.delete(context: Context) { @@ -309,17 +309,17 @@ fun Note.delete(context: Context) { deleteWithoutSync(context) return } - CoreConfig.instance.noteActions(this).delete(context) + ApplicationBase.instance.noteActions(this).delete(context) } fun Note.deleteWithoutSync(context: Context) { - CoreConfig.instance.noteActions(this).offlineDelete(context) + ApplicationBase.instance.noteActions(this).offlineDelete(context) } fun Note.deleteToSync(context: Context) { - CoreConfig.instance.noteActions(this).onlineDelete(context) + ApplicationBase.instance.noteActions(this).onlineDelete(context) } fun Note.softDelete(context: Context) { - CoreConfig.instance.noteActions(this).softDelete(context) + ApplicationBase.instance.noteActions(this).softDelete(context) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index f6549acc..b1ed4265 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -9,7 +9,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.util.RandomHelper import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getNoteState @@ -366,13 +366,13 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.view_distraction_free, icon = R.drawable.ic_action_distraction_free, listener = View.OnClickListener { - if (CoreConfig.instance.appFlavor() == Flavor.PRO) { + if (ApplicationBase.instance.appFlavor() == Flavor.PRO) { note.viewDistractionFree(activity) return@OnClickListener } com.maubis.scarlet.base.support.sheets.openSheet(activity, InstallProUpsellBottomSheet()) }, - visible = CoreConfig.instance.appFlavor() != Flavor.NONE, + visible = ApplicationBase.instance.appFlavor() != Flavor.NONE, invalid = activity.lockedContentIsHidden() && note.locked )) options.add(OptionsItem( @@ -380,7 +380,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.tap_for_action_popup, icon = R.drawable.ic_bubble_chart_white_48dp, listener = View.OnClickListener { - CoreConfig.instance.noteActions(note).popup(activity) + ApplicationBase.instance.noteActions(note).popup(activity) dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked @@ -390,11 +390,11 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.backup_note_enable, icon = R.drawable.ic_action_backup, listener = View.OnClickListener { - CoreConfig.instance.noteActions(note).enableBackup(activity) + ApplicationBase.instance.noteActions(note).enableBackup(activity) activity.updateNote(note) dismiss() }, - visible = note.disableBackup && CoreConfig.instance.appFlavor() != Flavor.NONE, + visible = note.disableBackup && ApplicationBase.instance.appFlavor() != Flavor.NONE, invalid = activity.lockedContentIsHidden() && note.locked )) options.add(OptionsItem( @@ -402,11 +402,11 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.backup_note_disable, icon = R.drawable.ic_action_backup_no, listener = View.OnClickListener { - CoreConfig.instance.noteActions(note).disableBackup(activity) + ApplicationBase.instance.noteActions(note).disableBackup(activity) activity.updateNote(note) dismiss() }, - visible = !note.disableBackup && CoreConfig.instance.appFlavor() != Flavor.NONE, + visible = !note.disableBackup && ApplicationBase.instance.appFlavor() != Flavor.NONE, invalid = activity.lockedContentIsHidden() && note.locked )) return options diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index 5cee9161..259d5cd8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -6,7 +6,7 @@ import android.speech.tts.TextToSpeech import android.widget.ImageView import android.widget.TextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.getUnreliablyStrippedText import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -26,10 +26,10 @@ class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { val nonNullNote = note!! val title = dialog.findViewById(R.id.options_title) - title.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) val speakPlayPause = dialog.findViewById(R.id.speak_play_pause) - speakPlayPause.setColorFilter(CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_ICON)) + speakPlayPause.setColorFilter(ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON)) speakPlayPause.setOnClickListener { val tts = textToSpeech if (tts === null) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/activity/INoteOptionSheetActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/activity/INoteOptionSheetActivity.kt index a231887b..648772f8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/activity/INoteOptionSheetActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/activity/INoteOptionSheetActivity.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.note.activity -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.database.room.note.Note interface INoteOptionSheetActivity { fun updateNote(note: Note) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index a71b09aa..ff31fbd0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -15,8 +15,11 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.format.MarkdownType -import com.maubis.scarlet.base.core.note.* +import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteImage.Companion.deleteIfExist +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.core.note.isEqual +import com.maubis.scarlet.base.core.note.isUnsaved import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.creation.specs.NoteCreationBottomBar import com.maubis.scarlet.base.note.creation.specs.NoteCreationTopBar diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 2b803239..0e76f79e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -12,7 +12,7 @@ import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewControllerItem import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder @@ -30,9 +30,11 @@ import com.maubis.scarlet.base.note.formats.IFormatRecyclerViewActivity import com.maubis.scarlet.base.note.formats.getFormatControllerItems import com.maubis.scarlet.base.note.formats.recycler.KEY_EDITABLE import com.maubis.scarlet.base.note.formats.recycler.KEY_NOTE_COLOR -import com.maubis.scarlet.base.settings.sheet.* +import com.maubis.scarlet.base.settings.sheet.STORE_KEY_TEXT_SIZE import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_ENABLED import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.useNoteColorAsBackground +import com.maubis.scarlet.base.settings.sheet.sEditorTextSize +import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.ui.* import com.maubis.scarlet.base.support.ui.ColorUtil.darkerColor @@ -80,6 +82,7 @@ open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity setContentView(R.layout.activity_advanced_note) context = this isDistractionFree = intent.getBooleanExtra(INTENT_KEY_DISTRACTION_FREE, false) + formats = emptyList().toMutableList() setRecyclerView() @@ -109,7 +112,7 @@ open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity override fun onResume() { super.onResume() - CoreConfig.instance.startListener(this) + ApplicationBase.instance.startListener(this) if (!creationFinished.get()) { return @@ -148,8 +151,8 @@ open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity val currentNote = note val bundle = Bundle() bundle.putBoolean(KEY_EDITABLE, editModeValue) - bundle.putBoolean(KEY_MARKDOWN_ENABLED, CoreConfig.instance.store().get(KEY_MARKDOWN_ENABLED, true)) - bundle.putBoolean(KEY_NIGHT_THEME, CoreConfig.instance.themeController().isNightTheme()) + bundle.putBoolean(KEY_MARKDOWN_ENABLED, ApplicationBase.instance.store().get(KEY_MARKDOWN_ENABLED, true)) + bundle.putBoolean(KEY_NIGHT_THEME, ApplicationBase.instance.themeController().isNightTheme()) bundle.putInt(STORE_KEY_TEXT_SIZE, sEditorTextSize) bundle.putInt(KEY_NOTE_COLOR, currentNote?.color ?: sNoteDefaultColor) bundle.putString(INTENT_KEY_NOTE_ID, currentNote?.uuid ?: generateUUID()) @@ -253,7 +256,7 @@ open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity return } - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() when { !useNoteColorAsBackground -> { colorConfig.backgroundColor = theme.get(ThemeColorType.BACKGROUND) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt index 0133a71b..e055ba81 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt @@ -3,7 +3,7 @@ package com.maubis.scarlet.base.note.creation.sheet import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem @@ -13,20 +13,20 @@ const val STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT = "editor_markdown_default" const val STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES = "editor_move_handles" var sEditorLiveMarkdown: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, true) - set(value) = CoreConfig.instance.store().put(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, value) var sEditorMarkdownDefault: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, false) - set(value) = CoreConfig.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, false) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, value) var sEditorMoveHandles: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, true) - set(value) = CoreConfig.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, value) var sEditorMarkdownEnabled: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, true) - set(value) = CoreConfig.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, value) class EditorOptionsBottomSheet : LithoOptionBottomSheet() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt index 2235e3d5..80e68eae 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt @@ -8,7 +8,6 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType -import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.core.note.NoteImage.Companion.deleteIfExist import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.support.option.OptionsItem diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt index 37413ed0..f0605b94 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt @@ -8,7 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -28,7 +28,7 @@ class MarkdownHelpBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_normal) .marginDip(YogaEdge.HORIZONTAL, 20f) .paddingDip(YogaEdge.VERTICAL, 4f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) } return column.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt index 3252e542..ec37b12e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt @@ -5,9 +5,7 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout -import com.facebook.litho.annotations.Prop import com.facebook.yoga.YogaAlign -import com.maubis.scarlet.base.note.creation.activity.NoteViewColorConfig import com.maubis.scarlet.base.support.specs.EmptySpec @LayoutSpec diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt index e8948504..c616ebad 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt @@ -1,9 +1,9 @@ package com.maubis.scarlet.base.note.folder import com.github.bijoysingh.starter.util.DateFormatter -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb +import com.maubis.scarlet.base.database.room.folder.Folder import java.util.* fun Folder.saveIfUnique() { @@ -42,21 +42,21 @@ fun Folder.getDisplayTime(): String { **************************************************************************************/ fun Folder.save() { - CoreConfig.instance.folderActions(this).save() + ApplicationBase.instance.folderActions(this).save() } fun Folder.saveWithoutSync() { - CoreConfig.instance.folderActions(this).offlineSave() + ApplicationBase.instance.folderActions(this).offlineSave() } fun Folder.saveToSync() { - CoreConfig.instance.folderActions(this).onlineSave() + ApplicationBase.instance.folderActions(this).onlineSave() } fun Folder.delete() { - CoreConfig.instance.folderActions(this).delete() + ApplicationBase.instance.folderActions(this).delete() } fun Folder.deleteWithoutSync() { - CoreConfig.instance.folderActions(this).offlineDelete() + ApplicationBase.instance.folderActions(this).offlineDelete() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt index cbcd3789..61434c80 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt @@ -3,8 +3,8 @@ package com.maubis.scarlet.base.note.folder import android.content.Context import android.support.v4.content.ContextCompat import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb +import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ColorUtil diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt index 6a3b619b..1e641527 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt @@ -9,7 +9,7 @@ import android.widget.EditText import android.widget.TextView import com.google.android.flexbox.FlexboxLayout import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.folder.isUnsaved import com.maubis.scarlet.base.database.room.folder.Folder @@ -51,9 +51,9 @@ class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { val colorFlexbox = dialog.findViewById(R.id.color_flexbox) val colorCard = dialog.findViewById(R.id.core_color_card) - title.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterFolder.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterFolder.setHintTextColor(CoreConfig.instance.themeController().get(ThemeColorType.HINT_TEXT)) + title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + enterFolder.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + enterFolder.setHintTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)) title.setText(if (folder.isUnsaved()) R.string.folder_sheet_add_note else R.string.folder_sheet_edit_note) action.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt index 728810b0..e305890b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt @@ -5,13 +5,13 @@ import android.content.Context import android.content.DialogInterface import android.view.View import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb +import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb +import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.note.folder.FolderOptionsItem import com.maubis.scarlet.base.note.save -import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.visibility diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt index ee0c966f..86b9d09a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt @@ -7,7 +7,7 @@ import android.widget.LinearLayout import com.github.bijoysingh.uibasics.views.UIActionView import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.note.folder.FolderOptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -58,7 +58,7 @@ abstract class FolderOptionItemBottomSheetBase : ThemedBottomSheetFragment() { if (option.editable) { contentView.setActionResource(option.getEditIcon()); - contentView.setActionTint(CoreConfig.instance.themeController().get(ThemeColorType.HINT_TEXT)); + contentView.setActionTint(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)); contentView.setActionClickListener { option.editListener() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt index 99f269b8..4c69ec1f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt @@ -1,19 +1,14 @@ package com.maubis.scarlet.base.note.folder.sheet import android.app.Dialog -import android.content.Context -import android.content.DialogInterface import android.view.View import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb +import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.folder.FolderBuilder +import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.note.folder.FolderOptionsItem -import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity -import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.visibility diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index cd6a1fce..726cd0ca 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -12,8 +12,6 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.note.ImageLoadCallback -import com.maubis.scarlet.base.core.note.NoteImage -import com.maubis.scarlet.base.main.sheets.AlertBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteFormatDialog import com.maubis.scarlet.base.note.creation.sheet.FormatActionBottomSheet import com.maubis.scarlet.base.support.ui.visibility diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index d6427ef9..82acffd1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -6,7 +6,7 @@ import android.support.v4.content.ContextCompat import android.view.View import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID @@ -46,7 +46,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView val tertiaryTextColor: Int val iconColor: Int val hintTextColor: Int - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() val isLightBackground = ColorUtil.isLightColored(noteColor) when { !useNoteColorAsBackground -> { @@ -86,7 +86,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView } }(), backgroundColor = when (data.formatType) { - FormatType.CODE, FormatType.IMAGE -> CoreConfig.instance.themeController().get(context, R.color.code_light, R.color.code_dark) + FormatType.CODE, FormatType.IMAGE -> ApplicationBase.instance.themeController().get(context, R.color.code_light, R.color.code_dark) else -> ContextCompat.getColor(context, R.color.transparent) }, secondaryTextColor = secondaryTextColor, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt index cf6da641..8408918d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt @@ -12,9 +12,8 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.visibility import com.maubis.scarlet.base.support.utils.trim diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt index 6435b733..11ec928a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt @@ -4,6 +4,7 @@ import com.evernote.android.job.Job import com.evernote.android.job.JobManager import com.evernote.android.job.JobRequest import com.evernote.android.job.util.support.PersistableBundleCompat +import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.Reminder import com.maubis.scarlet.base.core.note.ReminderInterval import com.maubis.scarlet.base.core.note.getReminderV2 @@ -12,7 +13,6 @@ import com.maubis.scarlet.base.note.saveWithoutSync import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.notification.REMINDER_NOTIFICATION_CHANNEL_ID -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import java.util.* import java.util.concurrent.TimeUnit diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt index 73a97869..59da8de9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt @@ -8,7 +8,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.uibasics.views.UIActionView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.Reminder import com.maubis.scarlet.base.core.note.ReminderInterval import com.maubis.scarlet.base.core.note.getReminderV2 @@ -208,9 +208,9 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { val reminderTime = dialog.findViewById(R.id.reminder_time) val reminderRepeat = dialog.findViewById(R.id.reminder_repeat) - val iconColor = CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_ICON) - val textColor = CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT) - val titleColor = CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER) + val iconColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON) + val textColor = ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT) + val titleColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER) reminderDate.setTitleColor(titleColor) reminderDate.setSubtitleColor(textColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt index 29af1e5b..9f4bc4cb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt @@ -5,12 +5,12 @@ import android.support.design.widget.FloatingActionButton import android.support.v7.widget.RecyclerView import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.main.HomeNavigationState import com.maubis.scarlet.base.note.getFullText import com.maubis.scarlet.base.note.selection.sheet.SelectedNoteOptionsBottomSheet import com.maubis.scarlet.base.support.utils.bind -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb const val KEY_SELECT_EXTRA_MODE = "KEY_SELECT_EXTRA_MODE" const val KEY_SELECT_EXTRA_NOTE_ID = "KEY_SELECT_EXTRA_NOTE_ID" diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index ea3af1c1..39bfc30f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -11,9 +11,9 @@ import android.widget.TextView import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.sort +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.main.recycler.EmptyRecyclerItem import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.note.recycler.NoteRecyclerItem @@ -66,11 +66,11 @@ abstract class SelectableNotesActivityBase : ThemedActivity(), INoteSelectorActi open fun getLayoutUI(): Int = R.layout.activity_select_note fun setupRecyclerView() { - val staggeredView = CoreConfig.instance.store().get(UISettingsOptionsBottomSheet.KEY_LIST_VIEW, false) + val staggeredView = ApplicationBase.instance.store().get(UISettingsOptionsBottomSheet.KEY_LIST_VIEW, false) val isTablet = resources.getBoolean(R.bool.is_tablet) - val isMarkdownEnabled = CoreConfig.instance.store().get(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) - val isMarkdownHomeEnabled = CoreConfig.instance.store().get(SettingsOptionsBottomSheet.KEY_MARKDOWN_HOME_ENABLED, true) + val isMarkdownEnabled = ApplicationBase.instance.store().get(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) + val isMarkdownHomeEnabled = ApplicationBase.instance.store().get(SettingsOptionsBottomSheet.KEY_MARKDOWN_HOME_ENABLED, true) val adapterExtra = Bundle() adapterExtra.putBoolean(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, isMarkdownEnabled && isMarkdownHomeEnabled) adapterExtra.putInt(STORE_KEY_LINE_COUNT, sNoteItemLineCount) @@ -90,7 +90,7 @@ abstract class SelectableNotesActivityBase : ThemedActivity(), INoteSelectorActi val containerLayout = findViewById(R.id.container_layout) containerLayout.setBackgroundColor(getThemeColor()) - val toolbarIconColor = CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_ICON); + val toolbarIconColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON); findViewById(R.id.back_button).setColorFilter(toolbarIconColor) findViewById(R.id.toolbar_title).setTextColor(toolbarIconColor) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/TagExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/TagExtensions.kt index af06ec05..6ac2451b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/TagExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/TagExtensions.kt @@ -1,8 +1,8 @@ package com.maubis.scarlet.base.note.tag -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb +import com.maubis.scarlet.base.database.room.tag.Tag fun Tag.saveIfUnique() { val existing = tagsDb.getByTitle(title) @@ -26,21 +26,21 @@ fun Tag.saveIfUnique() { **************************************************************************************/ fun Tag.save() { - CoreConfig.instance.tagActions(this).save() + ApplicationBase.instance.tagActions(this).save() } fun Tag.saveWithoutSync() { - CoreConfig.instance.tagActions(this).offlineSave() + ApplicationBase.instance.tagActions(this).offlineSave() } fun Tag.saveToSync() { - CoreConfig.instance.tagActions(this).onlineSave() + ApplicationBase.instance.tagActions(this).onlineSave() } fun Tag.delete() { - CoreConfig.instance.tagActions(this).delete() + ApplicationBase.instance.tagActions(this).delete() } fun Tag.deleteWithoutSync() { - CoreConfig.instance.tagActions(this).offlineDelete() + ApplicationBase.instance.tagActions(this).offlineDelete() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt index 8718f9f1..eba2a680 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt @@ -6,7 +6,7 @@ import android.view.View.VISIBLE import android.widget.EditText import android.widget.TextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.tag.isUnsaved import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.note.tag.delete @@ -43,9 +43,9 @@ class CreateOrEditTagBottomSheet : ThemedBottomSheetFragment() { val enterTag = dialog.findViewById(R.id.enter_tag) val removeBtn = dialog.findViewById(R.id.action_remove_button) - title.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterTag.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterTag.setHintTextColor(CoreConfig.instance.themeController().get(ThemeColorType.HINT_TEXT)) + title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + enterTag.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + enterTag.setHintTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)) title.setText(if (tag.isUnsaved()) R.string.tag_sheet_create_title else R.string.tag_sheet_edit_title) action.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt index 9748c2b4..10e34eaf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt @@ -3,12 +3,12 @@ package com.maubis.scarlet.base.note.tag.sheet import android.app.Dialog import android.view.View import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.note.getTagUUIDs import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity import com.maubis.scarlet.base.note.tag.TagOptionsItem -import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.visibility diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt index e692582b..dcadc63b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt @@ -4,13 +4,13 @@ import android.app.Dialog import android.content.DialogInterface import android.view.View import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.note.getTagUUIDs import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.note.tag.TagOptionsItem import com.maubis.scarlet.base.note.toggleTag -import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.visibility diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt index fecf5e9b..46fb83ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt @@ -7,7 +7,7 @@ import android.widget.LinearLayout import com.github.bijoysingh.uibasics.views.UIActionView import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.note.tag.TagOptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -56,7 +56,7 @@ abstract class TagOptionItemBottomSheetBase : ThemedBottomSheetFragment() { if (option.editable) { contentView.setActionResource(option.getEditIcon()); - contentView.setActionTint(CoreConfig.instance.themeController().get(ThemeColorType.HINT_TEXT)); + contentView.setActionTint(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)); contentView.setActionClickListener(option.editListener) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt index 1fc7fd1b..b05c79b2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt @@ -6,10 +6,10 @@ import android.widget.TextView import com.google.android.flexbox.FlexboxLayout import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.settings.view.ColorView import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb +import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.settings.view.ColorView class TagsAndColorPickerViewHolder( val activity: MainActivity, diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt index d1570510..18c54672 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt @@ -12,7 +12,7 @@ import android.widget.RemoteViews import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID @@ -93,7 +93,7 @@ class NotificationHandler(val context: Context) { contentView.setTextViewText(R.id.description, config.note.getText()) contentView.setTextViewText(R.id.timestamp, config.note.getDisplayTime()) - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val descColor = theme.get(ThemeColorType.TERTIARY_TEXT) contentView.setTextColor(R.id.title, titleColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt index bf5ed076..d2052e94 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt @@ -4,7 +4,7 @@ import android.app.IntentService import android.app.NotificationManager import android.content.Context import android.content.Intent -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.support.INTENT_KEY_ACTION import com.maubis.scarlet.base.support.INTENT_KEY_NOTE_ID @@ -37,10 +37,10 @@ class NotificationIntentService : IntentService("NotificationIntentService") { } when (action) { - NoteAction.COPY -> CoreConfig.instance.noteActions(note).copy(context) - NoteAction.SHARE -> CoreConfig.instance.noteActions(note).share(context) + NoteAction.COPY -> ApplicationBase.instance.noteActions(note).copy(context) + NoteAction.SHARE -> ApplicationBase.instance.noteActions(note).share(context) NoteAction.DELETE -> { - CoreConfig.instance.noteActions(note).softDelete(context) + ApplicationBase.instance.noteActions(note).softDelete(context) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.cancel(note.uid) } diff --git a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt index ef24d5f4..9586c8bc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt @@ -13,7 +13,7 @@ import com.bsk.floatingbubblelib.FloatingBubblePermissions import com.bsk.floatingbubblelib.FloatingBubbleService import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.database.room.note.Note @@ -36,7 +36,7 @@ class FloatingNoteService : FloatingBubbleService() { private lateinit var panel: View override fun getConfig(): FloatingBubbleConfig { - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() return FloatingBubbleConfig.Builder() .bubbleIcon(ContextCompat.getDrawable(context, R.drawable.app_icon)) .removeBubbleIcon(ContextCompat.getDrawable( @@ -69,7 +69,7 @@ class FloatingNoteService : FloatingBubbleService() { stopSelf() } - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() val rootView = getInflater().inflate(R.layout.layout_add_note_overlay, null) title = rootView.findViewById(R.id.title) as TextView diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt index d46214f5..62c700e8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt @@ -7,7 +7,7 @@ import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem @@ -50,7 +50,7 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { Uri.parse(PRIVACY_POLICY_LINK))) dismiss() }, - visible = CoreConfig.instance.appFlavor() != Flavor.NONE + visible = ApplicationBase.instance.appFlavor() != Flavor.NONE )) options.add(LithoOptionsItem( diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index 678d26d8..abd59e23 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -9,6 +9,7 @@ import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -42,29 +43,29 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutUsDetails) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_about_app) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutAppDetails) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_app_version) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(version) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.about_page_rate) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt index 4aff64db..98503353 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -86,7 +86,7 @@ class DeleteAndMoreOptionsBottomSheet : LithoOptionBottomSheet() { } )) - val forgetMeClick = CoreConfig.instance.authenticator().openForgetMeActivity(activity) + val forgetMeClick = ApplicationBase.instance.authenticator().openForgetMeActivity(activity) options.add(LithoOptionsItem( title = R.string.forget_me_option_title, subtitle = R.string.forget_me_option_details, @@ -95,7 +95,7 @@ class DeleteAndMoreOptionsBottomSheet : LithoOptionBottomSheet() { forgetMeClick?.run() dismiss() }, - visible = forgetMeClick !== null && CoreConfig.instance.authenticator().isLoggedIn() + visible = forgetMeClick !== null && ApplicationBase.instance.authenticator().isLoggedIn() )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt index 03b9f313..f7a0aa0f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt @@ -8,7 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -21,8 +21,8 @@ const val TEXT_SIZE_MIN = 12 const val TEXT_SIZE_MAX = 24 var sEditorTextSize: Int - get() = CoreConfig.instance.store().get(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) - set(value) = CoreConfig.instance.store().put(STORE_KEY_TEXT_SIZE, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_TEXT_SIZE, value) class FontSizeBottomSheet : LithoBottomSheet() { @@ -39,7 +39,7 @@ class FontSizeBottomSheet : LithoBottomSheet() { .textSizeDip(sEditorTextSize.toFloat()) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.note_option_font_size_example) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(CounterChooser.create(componentContext) .value(sEditorTextSize) .minValue(TEXT_SIZE_MIN) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt index f54ca47e..e647f541 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt @@ -7,7 +7,7 @@ import com.facebook.litho.ComponentContext import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -22,8 +22,8 @@ const val LINE_COUNT_MIN = 2 const val LINE_COUNT_MAX = 15 var sNoteItemLineCount: Int - get() = CoreConfig.instance.store().get(STORE_KEY_LINE_COUNT, LINE_COUNT_DEFAULT) - set(value) = CoreConfig.instance.store().put(STORE_KEY_LINE_COUNT, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_LINE_COUNT, LINE_COUNT_DEFAULT) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_LINE_COUNT, value) class LineCountBottomSheet : LithoBottomSheet() { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt index 4bad7c51..3efbb3a2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet @@ -12,8 +12,8 @@ import com.maubis.scarlet.base.support.sheets.openSheet const val STORE_KEY_NOTE_DEFAULT_COLOR = "KEY_NOTE_DEFAULT_COLOR" var sNoteDefaultColor: Int - get() = CoreConfig.instance.store().get(STORE_KEY_NOTE_DEFAULT_COLOR, (0xFFD32F2F).toInt()) - set(value) = CoreConfig.instance.store().put(STORE_KEY_NOTE_DEFAULT_COLOR, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_NOTE_DEFAULT_COLOR, (0xFFD32F2F).toInt()) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_NOTE_DEFAULT_COLOR, value) class NoteSettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.home_option_note_settings diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 8e7dfcd6..a6ec5d2c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -11,6 +11,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -35,18 +36,18 @@ class OpenSourceBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(openSourceDetails) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.osp_page_libraries) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 4f) .text(Markdown.render(LIBRARY_DETAILS_MD, true)) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.about_page_contribute) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index dc46385c..b4d954b1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -6,7 +6,7 @@ import com.github.ajalt.reprint.core.Reprint import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet.Companion.openCreateSheet import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet.Companion.openVerifySheet @@ -24,7 +24,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.security_option_set_pin_code_subtitle, icon = R.drawable.ic_option_security, listener = { - val currentPinCode = CoreConfig.instance.store().get(KEY_SECURITY_CODE, "") + val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") val hasPinCode = !TextUtils.isNullOrEmpty(currentPinCode) if (hasPinCode) { openResetPasswordDialog(dialog) @@ -33,7 +33,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { } }, isSelectable = true, - selected = !TextUtils.isNullOrEmpty(CoreConfig.instance.store().get(KEY_SECURITY_CODE, "")) + selected = !TextUtils.isNullOrEmpty(ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "")) )) val hasFingerprint = Reprint.hasFingerprintRegistered() options.add(LithoOptionsItem( @@ -41,23 +41,23 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.security_option_fingerprint_enabled_subtitle, icon = R.drawable.ic_option_fingerprint, listener = { - val currentPinCode = CoreConfig.instance.store().get(KEY_SECURITY_CODE, "") + val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") val hasPinCode = !TextUtils.isNullOrEmpty(currentPinCode) if (hasPinCode) { openVerifyPasswordDialog( object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { override fun onSuccess() { - CoreConfig.instance.store().put(KEY_FINGERPRINT_ENABLED, false) + ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, false) reset(componentContext.androidContext, dialog) } } ) } else { - CoreConfig.instance.store().put(KEY_FINGERPRINT_ENABLED, false) + ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, false) reset(componentContext.androidContext, dialog) } }, - visible = CoreConfig.instance.store().get(KEY_FINGERPRINT_ENABLED, true) && hasFingerprint, + visible = ApplicationBase.instance.store().get(KEY_FINGERPRINT_ENABLED, true) && hasFingerprint, isSelectable = true, selected = true )) @@ -66,23 +66,23 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.security_option_fingerprint_disabled_subtitle, icon = R.drawable.ic_option_fingerprint, listener = { - val currentPinCode = CoreConfig.instance.store().get(KEY_SECURITY_CODE, "") + val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") val hasPinCode = !TextUtils.isNullOrEmpty(currentPinCode) if (hasPinCode) { openVerifyPasswordDialog( object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { override fun onSuccess() { - CoreConfig.instance.store().put(KEY_FINGERPRINT_ENABLED, true) + ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, true) reset(componentContext.androidContext, dialog) } } ) } else { - CoreConfig.instance.store().put(KEY_FINGERPRINT_ENABLED, true) + ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, true) reset(componentContext.androidContext, dialog) } }, - visible = !CoreConfig.instance.store().get(KEY_FINGERPRINT_ENABLED, true) && hasFingerprint + visible = !ApplicationBase.instance.store().get(KEY_FINGERPRINT_ENABLED, true) && hasFingerprint )) return options } @@ -139,7 +139,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { } fun hasPinCodeEnabled(): Boolean { - val currentPinCode = CoreConfig.instance.store().get(KEY_SECURITY_CODE, "") + val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") return !TextUtils.isNullOrEmpty(currentPinCode) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index a1966ad7..ad8d6918 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -7,7 +7,7 @@ import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.sheet.BackupSettingsOptionsBottomSheet import com.maubis.scarlet.base.main.recycler.getMigrateToProAppInformationItem import com.maubis.scarlet.base.note.creation.sheet.EditorOptionsBottomSheet @@ -27,8 +27,8 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { val activity = context as MainActivity val options = ArrayList() - val loginClick = CoreConfig.instance.authenticator().openLoginActivity(activity) - val firebaseUser = CoreConfig.instance.authenticator().userId() + val loginClick = ApplicationBase.instance.authenticator().openLoginActivity(activity) + val firebaseUser = ApplicationBase.instance.authenticator().userId() val migrateToPro = getMigrateToProAppInformationItem(activity) options.add(LithoOptionsItem( @@ -39,7 +39,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { migrateToPro.function() dismiss() }, - visible = CoreConfig.instance.appFlavor() == Flavor.LITE && FlavourUtils.hasProAppInstalled(activity), + visible = ApplicationBase.instance.appFlavor() == Flavor.LITE && FlavourUtils.hasProAppInstalled(activity), selected = true )) options.add(LithoOptionsItem( @@ -108,7 +108,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { IntentUtils.openAppPlayStore(context, PRO_APP_PACKAGE_NAME) dismiss() }, - visible = CoreConfig.instance.appFlavor() == Flavor.LITE && !hasProAppInstalled(activity) + visible = ApplicationBase.instance.appFlavor() == Flavor.LITE && !hasProAppInstalled(activity) )) options.add(LithoOptionsItem( title = R.string.home_option_rate_and_review, @@ -144,7 +144,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.home_option_logout_of_app_subtitle, icon = R.drawable.ic_sign_in_options, listener = { - CoreConfig.instance.authenticator().logout() + ApplicationBase.instance.authenticator().logout() dismiss() }, visible = firebaseUser !== null diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt index 7251ae9a..c2887f38 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.core.note.SortingTechnique import com.maubis.scarlet.base.support.sheets.LithoChooseOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoChooseOptionsItem @@ -62,7 +62,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { const val KEY_SORTING_TECHNIQUE = "KEY_SORTING_TECHNIQUE" fun getSortingState(): SortingTechnique { - return SortingTechnique.values()[CoreConfig.instance.store().get(KEY_SORTING_TECHNIQUE, SortingTechnique.NEWEST_FIRST.ordinal)] + return SortingTechnique.values()[ApplicationBase.instance.store().get(KEY_SORTING_TECHNIQUE, SortingTechnique.NEWEST_FIRST.ordinal)] } fun getSortingTechniqueLabel(technique: SortingTechnique): Int { @@ -75,7 +75,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { } fun setSortingState(sortingTechnique: SortingTechnique) { - CoreConfig.instance.store().put(KEY_SORTING_TECHNIQUE, sortingTechnique.ordinal) + ApplicationBase.instance.store().put(KEY_SORTING_TECHNIQUE, sortingTechnique.ordinal) } fun openSheet(activity: MainActivity, listener: () -> Unit) { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index fb17fb3a..d83937cd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -10,7 +10,7 @@ import com.facebook.litho.annotations.Prop import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -92,7 +92,7 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { } val disabled = when { - CoreConfig.instance.appFlavor() == Flavor.PRO -> false + ApplicationBase.instance.appFlavor() == Flavor.PRO -> false theme == Theme.DARK || theme == Theme.LIGHT -> false else -> true } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index ebff2d37..3228a131 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet.Companion.getSortingState import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet.Companion.getSortingTechniqueLabel @@ -20,16 +20,16 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = componentContext.androidContext as MainActivity val options = ArrayList() - val flavor = CoreConfig.instance.appFlavor() + val flavor = ApplicationBase.instance.appFlavor() options.add(LithoOptionsItem( title = R.string.home_option_theme_color, subtitle = R.string.home_option_theme_color_subtitle, - icon = if (CoreConfig.instance.themeController().isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, + icon = if (ApplicationBase.instance.themeController().isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, listener = { com.maubis.scarlet.base.support.sheets.openSheet(activity, ThemeColorPickerBottomSheet().apply { this.onThemeChange = { theme -> - CoreConfig.instance.store().put(KEY_APP_THEME, theme.name) - CoreConfig.instance.themeController().notifyChange(activity) + ApplicationBase.instance.store().put(KEY_APP_THEME, theme.name) + ApplicationBase.instance.themeController().notifyChange(activity) activity.notifyThemeChange() reset(activity, dialog) } @@ -138,15 +138,15 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { } var useGridView: Boolean - get() = CoreConfig.instance.store().get(KEY_LIST_VIEW, true) - set(isGrid) = CoreConfig.instance.store().put(KEY_LIST_VIEW, isGrid) + get() = ApplicationBase.instance.store().get(KEY_LIST_VIEW, true) + set(isGrid) = ApplicationBase.instance.store().put(KEY_LIST_VIEW, isGrid) var useNoteColorAsBackground: Boolean - get() = CoreConfig.instance.store().get(KEY_NOTE_VIEWER_BG_COLOR, false) - set(value) = CoreConfig.instance.store().put(KEY_NOTE_VIEWER_BG_COLOR, value) + get() = ApplicationBase.instance.store().get(KEY_NOTE_VIEWER_BG_COLOR, false) + set(value) = ApplicationBase.instance.store().put(KEY_NOTE_VIEWER_BG_COLOR, value) var sMarkdownEnabledHome: Boolean - get() = CoreConfig.instance.store().get(KEY_MARKDOWN_HOME_ENABLED, true) - set(value) = CoreConfig.instance.store().put(KEY_MARKDOWN_HOME_ENABLED, value) + get() = ApplicationBase.instance.store().get(KEY_MARKDOWN_HOME_ENABLED, true) + set(value) = ApplicationBase.instance.store().put(KEY_MARKDOWN_HOME_ENABLED, value) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt index ec5a9ac0..40902d67 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt @@ -1,7 +1,6 @@ package com.maubis.scarlet.base.support.database import android.content.Context -import com.github.bijoysingh.starter.async.SimpleThreadExecutor import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteImage.Companion.deleteIfExist diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 15b5017e..46406206 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.support.database import android.content.Context import com.google.gson.Gson -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteMeta import com.maubis.scarlet.base.core.note.Reminder @@ -26,9 +26,9 @@ class Migrator(val context: Context) { fun start() { runTask(KEY_MIGRATE_THEME) { - val isNightMode = CoreConfig.instance.store().get(KEY_NIGHT_THEME, true) - CoreConfig.instance.store().put(KEY_APP_THEME, if (isNightMode) Theme.DARK.name else Theme.LIGHT.name) - CoreConfig.instance.themeController().notifyChange(context) + val isNightMode = ApplicationBase.instance.store().get(KEY_NIGHT_THEME, true) + ApplicationBase.instance.store().put(KEY_APP_THEME, if (isNightMode) Theme.DARK.name else Theme.LIGHT.name) + ApplicationBase.instance.themeController().notifyChange(context) } runTask(key = KEY_MIGRATE_REMINDERS) { val notes = notesDb.getAll() @@ -59,13 +59,13 @@ class Migrator(val context: Context) { runTaskIf( getLastUsedAppVersionCode() == 0, KEY_MIGRATE_DEFAULT_VALUES) { - CoreConfig.instance.store().put(KEY_APP_THEME, Theme.DARK.name) - CoreConfig.instance.store().put(KEY_LIST_VIEW, true) + ApplicationBase.instance.store().put(KEY_APP_THEME, Theme.DARK.name) + ApplicationBase.instance.store().put(KEY_LIST_VIEW, true) } } private fun runTask(key: String, task: () -> Unit) { - if (CoreConfig.instance.store().get(key, false)) { + if (ApplicationBase.instance.store().get(key, false)) { return } @@ -73,7 +73,7 @@ class Migrator(val context: Context) { task() } catch (_: Exception) { } - CoreConfig.instance.store().put(key, true) + ApplicationBase.instance.store().put(key, true) } private fun runTaskIf(condition: Boolean, key: String, task: () -> Unit) { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt index c19e08c1..3322ee5b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt @@ -6,7 +6,7 @@ import android.widget.GridLayout import android.widget.TextView import com.github.bijoysingh.uibasics.views.UILabelView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -36,7 +36,7 @@ abstract class GridBottomSheetBase : ThemedBottomSheetFragment() { fun setOptionTitle(dialog: Dialog, title: Int) { GlobalScope.launch(Dispatchers.Main) { val titleView = dialog.findViewById(R.id.options_title) - titleView.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + titleView.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) titleView.setText(title) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 1b2341aa..8e485be0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -18,6 +18,7 @@ import com.facebook.litho.widget.VerticalScroll import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.ui.BottomSheetTabletDialog @@ -35,7 +36,7 @@ fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { .marginDip(YogaEdge.TOP, 18f) .marginDip(YogaEdge.BOTTOM, 8f) .textStyle(Typeface.BOLD) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) } fun getLithoBottomSheetButton(context: ComponentContext): Text.Builder { @@ -81,7 +82,7 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { } fun getFullComponent(componentContext: ComponentContext, dialog: Dialog, childComponent: Component) { - val topHandle = when (CoreConfig.instance.themeController().isNightTheme()) { + val topHandle = when (ApplicationBase.instance.themeController().isNightTheme()) { true -> R.drawable.bottom_sheet_top_handle_dark false -> R.drawable.bottom_sheet_top_handle_light } @@ -90,7 +91,7 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { .paddingDip(YogaEdge.VERTICAL, 16f) .widthPercent(100f) .alignItems(YogaAlign.CENTER) - .backgroundColor(CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) .child( Image.create(componentContext) .drawableRes(topHandle) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt index 002455a5..350577b1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt @@ -12,6 +12,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -26,7 +27,7 @@ object ChooseOptionItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoChooseOptionsItem): Component { - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val selectedColor = context.getColor(R.color.colorAccent) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt index df84e5bc..1c722ff1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt @@ -12,7 +12,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_OPEN_SANS import com.maubis.scarlet.base.support.specs.RoundIcon @@ -34,7 +34,7 @@ object OptionItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoOptionsItem): Component { - val theme = CoreConfig.instance.themeController() + val theme = ApplicationBase.instance.themeController() val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val subtitleColor = theme.get(ThemeColorType.HINT_TEXT) val selectedColor = theme.get(ThemeColorType.ACCENT_TEXT) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt index 45c051ca..9022fe2b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt @@ -9,6 +9,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetButton import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -35,7 +36,7 @@ object BottomSheetBarSpec { .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) .clickHandler(BottomSheetBar.onSecondaryClickEvent(context))) } row.child(EmptySpec.create(context).flexGrow(1f)) @@ -47,7 +48,7 @@ object BottomSheetBarSpec { .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) .clickHandler(BottomSheetBar.onTertiaryClickEvent(context))) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt index 8605272a..f7a87a1b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt @@ -10,6 +10,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -33,7 +34,7 @@ object CounterChooserSpec { .typeface(CoreConfig.FONT_MONSERRAT) .textSizeRes(R.dimen.font_size_xxxlarge) .paddingDip(YogaEdge.HORIZONTAL, 12f) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(bottomBarRoundIcon(context, ToolbarColorConfig()) .iconRes(R.drawable.icon_more_counter) .onClick { onValueChange(Math.min(value + 1, maxValue)) }) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt index bf2533c5..5bba0317 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt @@ -1,6 +1,5 @@ package com.maubis.scarlet.base.support.specs -import android.graphics.Color import android.graphics.drawable.Drawable import com.facebook.litho.ClickEvent import com.facebook.litho.Column diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt index 5d4771a2..3ab7a470 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt @@ -12,7 +12,7 @@ import com.facebook.litho.widget.SolidColor import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.ui.ThemeColorType object EmptySpec { @@ -41,8 +41,8 @@ fun separatorSpec(context: ComponentContext): Component.Builder<*> { data class ToolbarColorConfig( - var toolbarBackgroundColor: Int = CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_BACKGROUND), - var toolbarIconColor: Int = CoreConfig.instance.themeController().get(ThemeColorType.TOOLBAR_ICON)) + var toolbarBackgroundColor: Int = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_BACKGROUND), + var toolbarIconColor: Int = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON)) fun bottomBarRoundIcon(context: ComponentContext, colorConfig: ToolbarColorConfig): RoundIcon.Builder { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt index e1e9aa81..83f58bfe 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.support.ui import android.graphics.* import android.graphics.drawable.Drawable -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase class CircleDrawable(color: Int, showBorder: Boolean = true) : Drawable() { private val paint: Paint @@ -13,7 +13,7 @@ class CircleDrawable(color: Int, showBorder: Boolean = true) : Drawable() { this.paint = Paint(Paint.ANTI_ALIAS_FLAG) this.paint.color = color - val isNightTheme = CoreConfig.instance.themeController().isNightTheme() + val isNightTheme = ApplicationBase.instance.themeController().isNightTheme() this.borderPaint = Paint(Paint.ANTI_ALIAS_FLAG) this.borderPaint.color = when { !showBorder -> Color.TRANSPARENT diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt index 1823192b..6b9bbb6e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.support.ui import android.graphics.* import com.facebook.litho.drawable.ComparableDrawable -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean = false) : ComparableDrawable() { private val mPaint: Paint @@ -14,7 +14,7 @@ class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean this.mPaint.color = color this.mPaint.alpha = alpha - val isNightTheme = CoreConfig.instance.themeController().isNightTheme() + val isNightTheme = ApplicationBase.instance.themeController().isNightTheme() this.mBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG) this.mBorderPaint.color = when { !showBorder -> Color.TRANSPARENT diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index 2809cb41..9ca1d90d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -7,7 +7,7 @@ import android.support.v4.content.ContextCompat import com.github.bijoysingh.starter.util.DimensionManager import com.maubis.markdown.MarkdownConfig import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase const val KEY_APP_THEME = "KEY_APP_THEME" @@ -94,7 +94,7 @@ class ThemeManager() : IThemeManager { } fun getThemeFromStore(): Theme { - val theme = CoreConfig.instance.store().get(KEY_APP_THEME, Theme.DARK.name) + val theme = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) try { return Theme.valueOf(theme) } catch (_: Exception) { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index 03530e9d..a2ae1bad 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -5,7 +5,7 @@ import android.os.Build import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase abstract class ThemedActivity : AppCompatActivity() { @@ -26,7 +26,7 @@ abstract class ThemedActivity : AppCompatActivity() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val view = window.decorView var flags = view.systemUiVisibility - flags = when (CoreConfig.instance.themeController().isNightTheme()) { + flags = when (ApplicationBase.instance.themeController().isNightTheme()) { true -> flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() false -> flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } @@ -34,9 +34,9 @@ abstract class ThemedActivity : AppCompatActivity() { } } - fun getThemeColor(): Int = CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND) + fun getThemeColor(): Int = ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND) - fun getStatusBarColor(): Int = CoreConfig.instance.themeController().get(ThemeColorType.STATUS_BAR) + fun getStatusBarColor(): Int = ApplicationBase.instance.themeController().get(ThemeColorType.STATUS_BAR) fun tryClosingTheKeyboard() { try { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt index 0c8dcda1..9d49afe8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt @@ -12,8 +12,7 @@ import android.view.View import com.github.bijoysingh.starter.fragments.SimpleBottomSheetFragment import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.export.sheet.BackupSettingsOptionsBottomSheet +import com.maubis.scarlet.base.config.ApplicationBase abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { @@ -43,7 +42,7 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { abstract fun getBackgroundView(): Int fun resetBackground(dialog: Dialog) { - val backgroundColor = CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND) + val backgroundColor = ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND) val containerLayout = dialog.findViewById(getBackgroundView()) containerLayout.setBackgroundColor(backgroundColor) for (viewId in getBackgroundCardViewIds()) { @@ -54,8 +53,8 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { open fun getOptionsTitleColor(selected: Boolean): Int { val colorResource = when { - CoreConfig.instance.themeController().isNightTheme() && selected -> R.color.material_blue_300 - CoreConfig.instance.themeController().isNightTheme() -> R.color.light_secondary_text + ApplicationBase.instance.themeController().isNightTheme() && selected -> R.color.material_blue_300 + ApplicationBase.instance.themeController().isNightTheme() -> R.color.light_secondary_text selected -> R.color.material_blue_700 else -> R.color.dark_secondary_text } @@ -64,8 +63,8 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { open fun getOptionsSubtitleColor(selected: Boolean): Int { val colorResource = when { - CoreConfig.instance.themeController().isNightTheme() && selected -> R.color.material_blue_200 - CoreConfig.instance.themeController().isNightTheme() -> R.color.light_tertiary_text + ApplicationBase.instance.themeController().isNightTheme() && selected -> R.color.material_blue_200 + ApplicationBase.instance.themeController().isNightTheme() -> R.color.light_tertiary_text selected -> R.color.material_blue_500 else -> R.color.dark_tertiary_text } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt index 9a63ffe5..c27eb0cb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.support.utils import com.maubis.scarlet.base.BuildConfig -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet.Companion.WHATS_NEW_UID import java.util.* @@ -20,7 +20,7 @@ fun getCurrentVersionCode(): Int { * If nothing can be concluded it's 0 (assumes new user) */ fun getLastUsedAppVersionCode(): Int { - val appVersion = CoreConfig.instance.store().get(KEY_LAST_KNOWN_APP_VERSION, 0) + val appVersion = ApplicationBase.instance.store().get(KEY_LAST_KNOWN_APP_VERSION, 0) return when { appVersion > 0 -> appVersion notesDb.getCount() > 0 -> -1 @@ -29,7 +29,7 @@ fun getLastUsedAppVersionCode(): Int { } fun shouldShowWhatsNewSheet(): Boolean { - val lastShownWhatsNew = CoreConfig.instance.store().get(KEY_LAST_SHOWN_WHATS_NEW, 0) + val lastShownWhatsNew = ApplicationBase.instance.store().get(KEY_LAST_SHOWN_WHATS_NEW, 0) if (lastShownWhatsNew >= WHATS_NEW_UID) { // Already shown the latest return false @@ -38,18 +38,18 @@ fun shouldShowWhatsNewSheet(): Boolean { val lastUsedAppVersion = getLastUsedAppVersionCode() // Update the values independent of the decision - CoreConfig.instance.store().put(KEY_LAST_SHOWN_WHATS_NEW, WHATS_NEW_UID) - CoreConfig.instance.store().put(KEY_LAST_KNOWN_APP_VERSION, getCurrentVersionCode()) + ApplicationBase.instance.store().put(KEY_LAST_SHOWN_WHATS_NEW, WHATS_NEW_UID) + ApplicationBase.instance.store().put(KEY_LAST_KNOWN_APP_VERSION, getCurrentVersionCode()) // New users don't need to see the whats new screen return lastUsedAppVersion != 0 } fun getInstanceID(): String { - val deviceId = CoreConfig.instance.store().get(KEY_INSTANCE_ID, "") + val deviceId = ApplicationBase.instance.store().get(KEY_INSTANCE_ID, "") if (deviceId.isBlank()) { val newDeviceId = UUID.randomUUID().toString() - CoreConfig.instance.store().put(KEY_INSTANCE_ID, newDeviceId) + ApplicationBase.instance.store().put(KEY_INSTANCE_ID, newDeviceId) return newDeviceId } return deviceId diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt index 74c1da8e..403dd503 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.support.utils import android.content.Context -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -26,8 +26,8 @@ object FlavourUtils { } catch (e: Exception) { found = false } - CoreConfig.instance.store().put(KEY_PRO_APP_INSTALLED, found) + ApplicationBase.instance.store().put(KEY_PRO_APP_INSTALLED, found) } - return CoreConfig.instance.store().get(KEY_PRO_APP_INSTALLED, false) + return ApplicationBase.instance.store().get(KEY_PRO_APP_INSTALLED, false) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt index 29acfe31..905526f4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt @@ -1,16 +1,16 @@ package com.maubis.scarlet.base.widget import android.app.Application +import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider +import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.Uri import android.widget.RemoteViews -import com.maubis.scarlet.base.R -import android.app.PendingIntent -import android.content.ComponentName import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.R import com.maubis.scarlet.base.note.creation.activity.CreateListNoteActivity import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/NoteWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/NoteWidgetProvider.kt index 3db4b7a7..1c4ef3ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/NoteWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/NoteWidgetProvider.kt @@ -3,7 +3,7 @@ package com.maubis.scarlet.base.widget import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.activity.WidgetConfigureActivity class NoteWidgetProvider : AppWidgetProvider() { @@ -12,7 +12,7 @@ class NoteWidgetProvider : AppWidgetProvider() { val N = appWidgetIds.size for (i in 0 until N) { val appWidgetId = appWidgetIds[i] - val widget = CoreConfig.instance.database().widgets().getByID(appWidgetId) + val widget = ApplicationBase.instance.database().widgets().getByID(appWidgetId) if (widget === null) { continue } @@ -30,11 +30,11 @@ class NoteWidgetProvider : AppWidgetProvider() { val N = appWidgetIds.size for (i in 0 until N) { val appWidgetId = appWidgetIds[i] - val widget = CoreConfig.instance.database().widgets().getByID(appWidgetId) + val widget = ApplicationBase.instance.database().widgets().getByID(appWidgetId) if (widget === null) { continue } - CoreConfig.instance.database().widgets().delete(widget) + ApplicationBase.instance.database().widgets().delete(widget) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index ec1baa0f..4d5a43e7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -9,6 +9,7 @@ import com.facebook.litho.ComponentContext import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.sort @@ -25,23 +26,23 @@ import kotlinx.coroutines.launch const val STORE_KEY_WIDGET_ENABLE_FORMATTING = "widget_enable_formatting" var sWidgetEnableFormatting: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_WIDGET_ENABLE_FORMATTING, true) - set(value) = CoreConfig.instance.store().put(STORE_KEY_WIDGET_ENABLE_FORMATTING, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_ENABLE_FORMATTING, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_ENABLE_FORMATTING, value) const val STORE_KEY_WIDGET_SHOW_LOCKED_NOTES = "widget_show_locked_notes" var sWidgetShowLockedNotes: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, false) - set(value) = CoreConfig.instance.store().put(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, false) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, value) const val STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES = "widget_show_archived_notes" var sWidgetShowArchivedNotes: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, true) - set(value) = CoreConfig.instance.store().put(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, value) const val STORE_KEY_WIDGET_SHOW_TRASH_NOTES = "widget_show_trash_notes" var sWidgetShowDeletedNotes: Boolean - get() = CoreConfig.instance.store().get(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, false) - set(value) = CoreConfig.instance.store().put(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, value) + get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, false) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, value) fun getWidgetNoteText(note: Note): CharSequence { if (note.locked && !sWidgetShowLockedNotes) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index 29e5f3e7..34403303 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -5,17 +5,16 @@ import com.bijoysingh.quicknote.firebase.FirebaseRemoteDatabase import com.bijoysingh.quicknote.scarlet.ScarletConfig import com.github.bijoysingh.starter.prefs.Store import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.export.support.ExternalFolderSync class Scarlet : ApplicationBase() { override fun onCreate() { super.onCreate() - CoreConfig.instance = ScarletConfig(this) - CoreConfig.instance.themeController().setup(this) - CoreConfig.instance.authenticator().setup(this) - CoreConfig.instance.remoteConfigFetcher().setup(this) + ApplicationBase.instance = ScarletConfig(this) + ApplicationBase.instance.themeController().setup(this) + ApplicationBase.instance.authenticator().setup(this) + ApplicationBase.instance.remoteConfigFetcher().setup(this) ExternalFolderSync.setup(this) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index f359fdc8..f66214cd 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -10,6 +10,7 @@ import com.facebook.litho.widget.VerticalScroll import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.support.ui.LithoCircleDrawable @@ -26,14 +27,14 @@ object GDriveRootViewSpec { else -> R.string.google_drive_page_login_button } return Column.create(context) - .backgroundColor(CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) .childComponent(GDriveContentView.create(context))) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.google_drive_page_login_firebase_button) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_MONSERRAT)) @@ -71,15 +72,15 @@ object GDriveContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(CoreConfig.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_login_title) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_login_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(GDriveIconView.create(context) @@ -115,7 +116,7 @@ object GDriveIconViewSpec { .text(title) .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) - .textColor(CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT)) .build() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index fadf6ff5..45617729 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -5,14 +5,11 @@ import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView -import com.github.bijoysingh.starter.prefs.Store -import com.github.bijoysingh.starter.prefs.VersionedStore import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -27,9 +24,7 @@ import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccoun import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.config.USER_PREFERENCES_STORE_NAME -import com.maubis.scarlet.base.config.USER_PREFERENCES_VERSION +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -139,7 +134,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed if (database === null) { return@launch } - CoreConfig.instance.notesDatabase().getAll().forEach { + ApplicationBase.instance.notesDatabase().getAll().forEach { val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.NOTE.name, it.uuid) ?: GDriveUploadData() existing.apply { @@ -148,7 +143,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed save(database) } } - CoreConfig.instance.tagsDatabase().getAll().forEach { + ApplicationBase.instance.tagsDatabase().getAll().forEach { val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.TAG.name, it.uuid) ?: GDriveUploadData() existing.apply { @@ -157,7 +152,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed save(database) } } - CoreConfig.instance.foldersDatabase().getAll().forEach { + ApplicationBase.instance.foldersDatabase().getAll().forEach { val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.FOLDER.name, it.uuid) ?: GDriveUploadData() existing.apply { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 5d8c0d78..ca6c6c4f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -10,8 +10,8 @@ import com.bijoysingh.quicknote.database.genGDriveUploadDatabase import com.bijoysingh.quicknote.firebase.data.* import com.github.bijoysingh.starter.prefs.Store import com.google.gson.Gson +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.IFolderContainer import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType @@ -88,13 +88,13 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { gDriveConfig = Store.get(context, "gdrive_config") notesSync = GDriveRemoteFolder(GDriveDataType.NOTE, gDriveDatabase!!, helper) { - CoreConfig.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() + ApplicationBase.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() } tagsSync = GDriveRemoteFolder(GDriveDataType.TAG, gDriveDatabase!!, helper) { - CoreConfig.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() + ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() } foldersSync = GDriveRemoteFolder(GDriveDataType.FOLDER, gDriveDatabase!!, helper) { - CoreConfig.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() + ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() } imageSync = GDriveRemoteImageFolder(GDriveDataType.IMAGE, gDriveDatabase!!, helper) @@ -297,7 +297,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (!sameUpdateTime) { when { sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - CoreConfig.instance.notesDatabase().getByUUID(it.uuid)?.getFirebaseNote()?.apply { + ApplicationBase.instance.notesDatabase().getByUUID(it.uuid)?.getFirebaseNote()?.apply { insert(this) } sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertNote(it) @@ -316,7 +316,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (!sameUpdateTime) { when { sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - CoreConfig.instance.tagsDatabase().getByUUID(it.uuid)?.getFirebaseTag()?.apply { + ApplicationBase.instance.tagsDatabase().getByUUID(it.uuid)?.getFirebaseTag()?.apply { insert(this) } sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertTag(it) @@ -335,7 +335,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (!sameUpdateTime) { when { sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - CoreConfig.instance.foldersDatabase().getByUUID(it.uuid)?.getFirebaseFolder()?.apply { + ApplicationBase.instance.foldersDatabase().getByUUID(it.uuid)?.getFirebaseFolder()?.apply { insert(this) } sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertFolder(it) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index adbee400..d8eceadc 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -1,9 +1,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.database.GDriveUploadDataDao -import com.google.api.services.drive.model.File import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt index 67c5c7d9..c5da563c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt @@ -11,12 +11,11 @@ import android.widget.TextView import com.bijoysingh.quicknote.R import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.ToastHelper -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.settings.sheet.AboutSettingsOptionsBottomSheet +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.settings.sheet.PRIVACY_POLICY_LINK -import com.maubis.scarlet.base.support.utils.bind import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.visibility +import com.maubis.scarlet.base.support.utils.bind const val KEY_DATA_POLICY_REQUEST = "KEY_DATA_POLICY_REQUEST" const val KEY_DATA_POLICY_REQUEST_LOGGED_IN = "LOGGED_IN" @@ -48,7 +47,7 @@ class DataPolicyActivity : ThemedActivity() { doneBtn.setOnClickListener { if (acceptCheckBox.isChecked) { acceptThePolicy() - if (startState == "" && !CoreConfig.instance.authenticator().isLoggedIn()) { + if (startState == "" && !ApplicationBase.instance.authenticator().isLoggedIn()) { IntentUtils.startActivity(this, LoginActivity::class.java) } @@ -62,7 +61,7 @@ class DataPolicyActivity : ThemedActivity() { refuseBtn.setOnClickListener { IntentUtils.startActivity(this, ForgetMeActivity::class.java) } - refuseBtn.visibility = visibility(CoreConfig.instance.authenticator().isLoggedIn()) + refuseBtn.visibility = visibility(ApplicationBase.instance.authenticator().isLoggedIn()) privacyPolicy.setOnClickListener { startActivity(Intent( @@ -77,7 +76,7 @@ class DataPolicyActivity : ThemedActivity() { override fun onResume() { super.onResume() - if (startState == KEY_DATA_POLICY_REQUEST_LOGGED_IN && !CoreConfig.instance.authenticator().isLoggedIn()) { + if (startState == KEY_DATA_POLICY_REQUEST_LOGGED_IN && !ApplicationBase.instance.authenticator().isLoggedIn()) { finish() } } @@ -87,12 +86,12 @@ class DataPolicyActivity : ThemedActivity() { const val DATA_POLICY_VERSION = 4 const val DATA_POLICY_ACCEPTED = "DATA_POLICY_ACCEPTED_VERSION" - fun hasAcceptedThePolicy() = CoreConfig.instance.store().get(DATA_POLICY_ACCEPTED, 0) == DATA_POLICY_VERSION + fun hasAcceptedThePolicy() = ApplicationBase.instance.store().get(DATA_POLICY_ACCEPTED, 0) == DATA_POLICY_VERSION - fun acceptThePolicy() = CoreConfig.instance.store().put(DATA_POLICY_ACCEPTED, DATA_POLICY_VERSION) + fun acceptThePolicy() = ApplicationBase.instance.store().put(DATA_POLICY_ACCEPTED, DATA_POLICY_VERSION) fun openIfNeeded(activity: AppCompatActivity) { - if (!hasAcceptedThePolicy() && CoreConfig.instance.authenticator().isLoggedIn()) { + if (!hasAcceptedThePolicy() && ApplicationBase.instance.authenticator().isLoggedIn()) { val intent = Intent(activity, DataPolicyActivity::class.java) intent.putExtra(KEY_DATA_POLICY_REQUEST, KEY_DATA_POLICY_REQUEST_LOGGED_IN) activity.startActivity(intent) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index a1293b60..cccc1e54 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -6,7 +6,6 @@ import android.support.v4.content.ContextCompat import android.widget.CheckBox import android.widget.TextView import com.bijoysingh.quicknote.R -import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -18,9 +17,9 @@ import com.google.android.gms.tasks.Task import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.support.utils.bind +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.bind class ForgetMeActivity : ThemedActivity() { @@ -51,7 +50,7 @@ class ForgetMeActivity : ThemedActivity() { return@setOnClickListener } - val userId = CoreConfig.instance.authenticator().userId() + val userId = ApplicationBase.instance.authenticator().userId() if (userId === null) { return@setOnClickListener } @@ -63,7 +62,7 @@ class ForgetMeActivity : ThemedActivity() { ?.delete() ?.addOnCompleteListener { if (it.isSuccessful) { - CoreConfig.instance.authenticator().logout() + ApplicationBase.instance.authenticator().logout() finish() return@addOnCompleteListener } @@ -145,7 +144,7 @@ class ForgetMeActivity : ThemedActivity() { return@addOnCompleteListener } - CoreConfig.instance.authenticator().logout() + ApplicationBase.instance.authenticator().logout() finish() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt index cc3945c7..4afda03c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt @@ -21,7 +21,7 @@ import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -83,7 +83,7 @@ class LoginActivity : ThemedActivity() { finish() } - val proInformationVisibility = if (CoreConfig.instance.appFlavor() == Flavor.PRO) View.GONE else View.VISIBLE + val proInformationVisibility = if (ApplicationBase.instance.appFlavor() == Flavor.PRO) View.GONE else View.VISIBLE installPro.visibility = proInformationVisibility val installProTitle = findViewById(R.id.install_pro_details_title) installProTitle.visibility = proInformationVisibility @@ -164,7 +164,7 @@ class LoginActivity : ThemedActivity() { } private fun onLoginSuccess(user: FirebaseUser?) { - CoreConfig.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) + ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) transitionNotesToServer(user) buttonTitle.setText(R.string.logged_into_app) } @@ -197,9 +197,9 @@ class LoginActivity : ThemedActivity() { containerLayout.setBackgroundColor(getThemeColor()); val optionsTitle = findViewById(R.id.sign_in_title) - optionsTitle.setTextColor(CoreConfig.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + optionsTitle.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - val textColor = CoreConfig.instance.themeController().get(ThemeColorType.TERTIARY_TEXT) + val textColor = ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT) val installProDescription = findViewById(R.id.install_pro_details_description) val cloudSyncDescription = findViewById(R.id.cloud_sync_details_description) installProDescription.setTextColor(textColor) @@ -207,7 +207,7 @@ class LoginActivity : ThemedActivity() { val installProTitle = findViewById(R.id.install_pro_details_title) val cloudSyncTitle = findViewById(R.id.cloud_sync_details_title) - val titleTextColor = CoreConfig.instance.themeController().get(ThemeColorType.SECTION_HEADER) + val titleTextColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER) installProTitle.setTextColor(titleTextColor) cloudSyncTitle.setTextColor(titleTextColor) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt index a4ea607c..0d6f3920 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt @@ -8,7 +8,7 @@ import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley import com.bijoysingh.quicknote.BuildConfig import com.google.gson.Gson -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.config.remote.RemoteConfig import com.maubis.scarlet.base.support.utils.Flavor @@ -21,16 +21,16 @@ const val KEY_RC_FULL_VERSION = "KEY_RC_FULL_VERSION" class RemoteConfigFetcher() : IRemoteConfigFetcher { override fun setup(context: Context) { - val lastFetched = CoreConfig.instance.store().get(KEY_REMOTE_CONFIG_FETCH_TIME, 0L) + val lastFetched = ApplicationBase.instance.store().get(KEY_REMOTE_CONFIG_FETCH_TIME, 0L) if (System.currentTimeMillis() > lastFetched + REMOTE_CONFIG_REFETCH_TIME_MS) { fetchConfig(context) } } override fun isLatestVersion(): Boolean { - val latestVersion = when (CoreConfig.instance.appFlavor()) { - Flavor.PRO -> CoreConfig.instance.store().get(KEY_RC_FULL_VERSION, 0) - Flavor.LITE -> CoreConfig.instance.store().get(KEY_RC_LITE_VERSION, 0) + val latestVersion = when (ApplicationBase.instance.appFlavor()) { + Flavor.PRO -> ApplicationBase.instance.store().get(KEY_RC_FULL_VERSION, 0) + Flavor.LITE -> ApplicationBase.instance.store().get(KEY_RC_LITE_VERSION, 0) else -> 0 } return BuildConfig.VERSION_CODE >= latestVersion @@ -55,11 +55,11 @@ class RemoteConfigFetcher() : IRemoteConfigFetcher { return } - CoreConfig.instance.store().put(KEY_REMOTE_CONFIG_FETCH_TIME, System.currentTimeMillis()) + ApplicationBase.instance.store().put(KEY_REMOTE_CONFIG_FETCH_TIME, System.currentTimeMillis()) try { val config = Gson().fromJson(response, RemoteConfig::class.java) - CoreConfig.instance.store().put(KEY_RC_LITE_VERSION, config.rc_lite_production_version ?: 0) - CoreConfig.instance.store().put(KEY_RC_FULL_VERSION, config.rc_full_production_version ?: 0) + ApplicationBase.instance.store().put(KEY_RC_LITE_VERSION, config.rc_lite_production_version ?: 0) + ApplicationBase.instance.store().put(KEY_RC_FULL_VERSION, config.rc_full_production_version ?: 0) } catch (exception: Exception) { return } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index f34e328d..14e486e1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -18,8 +18,8 @@ import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseNetworkException import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.FirebaseDatabase +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.auth.IAuthenticator -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -89,7 +89,7 @@ class ScarletAuthenticator() : IAuthenticator { } logout() - CoreConfig.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) + ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) val handler = Handler(Looper.getMainLooper()) handler.post { ToastHelper.show(context, "You have been signed out of the app") diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index 79cb89df..e0c6c868 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -7,15 +7,15 @@ import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.openIfNeeded import com.bijoysingh.quicknote.firebase.support.RemoteConfigFetcher import com.bijoysingh.quicknote.firebase.support.ScarletAuthenticator +import com.maubis.scarlet.base.config.MaterialNoteConfig import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher -import com.maubis.scarlet.base.config.MaterialNoteConfig -import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.core.folder.IFolderActor import com.maubis.scarlet.base.core.note.INoteActor import com.maubis.scarlet.base.core.tag.ITagActor +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.support.utils.Flavor class ScarletConfig(context: Context) : MaterialNoteConfig(context) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 389ae43a..78264425 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -3,8 +3,8 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseFolder -import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.core.folder.MaterialFolderActor +import com.maubis.scarlet.base.database.room.folder.Folder class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index 2c09c69b..d60d5871 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -4,8 +4,8 @@ import android.content.Context import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseNote -import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.core.note.MaterialNoteActor +import com.maubis.scarlet.base.database.room.note.Note class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 9e3bc8a2..40a53301 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -3,8 +3,8 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseTag -import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.core.tag.MaterialTagActor +import com.maubis.scarlet.base.database.room.tag.Tag class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { From 83fe75ef15a22d85b7489cccf1a13d899c495ac9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 15 May 2019 23:20:54 +0100 Subject: [PATCH 014/134] Fixing crash due to race condition --- .../base/note/creation/activity/CreateNoteActivity.kt | 2 +- .../scarlet/base/note/creation/activity/ViewNoteActivity.kt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index ff31fbd0..c49795a5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -184,7 +184,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { protected fun maybeUpdateNoteWithoutSync() { val currentNote = note - if (currentNote === null) { + if (currentNote === null || !formatsInitialised.get()) { return } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 0e76f79e..9f2e35f4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -60,11 +60,13 @@ data class NoteViewColorConfig( open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity, IFormatRecyclerViewActivity { var focusedFormat: Format? = null + protected var note: Note? = null + protected lateinit var formats: MutableList + protected val formatsInitialised = AtomicBoolean(false) protected lateinit var context: Context protected lateinit var adapter: FormatAdapter - protected lateinit var formats: MutableList protected lateinit var formatsView: RecyclerView protected var isDistractionFree: Boolean = false @@ -82,7 +84,6 @@ open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity setContentView(R.layout.activity_advanced_note) context = this isDistractionFree = intent.getBooleanExtra(INTENT_KEY_DISTRACTION_FREE, false) - formats = emptyList().toMutableList() setRecyclerView() @@ -177,6 +178,7 @@ open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity false -> currentNote.getSmartFormats() }.toMutableList() adapter.addItems(formats) + formatsInitialised.set(true) if (!editModeValue) { maybeAddTags() From 87d130b7dea4c7688dc4efc13e6a2eeb191060f9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 15 May 2019 23:26:33 +0100 Subject: [PATCH 015/134] [CrashFix] Fixing crash due to saveImage --- .../scarlet/base/support/utils/ImageCache.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt index 2a6ec502..a160b0e7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt @@ -75,12 +75,18 @@ class ImageCache(context: Context) { thumbnailCacheSize.addAndGet(-cacheFile.length()) } - val fOut = FileOutputStream(cacheFile) - val compressedBitmap = sampleBitmap(bitmap) - compressedBitmap.compress(Bitmap.CompressFormat.PNG, 75, fOut) - fOut.flush() - fOut.close() + val compressedBitmap: Bitmap = sampleBitmap(bitmap) + + try { + val fOut = FileOutputStream(cacheFile) + compressedBitmap.compress(Bitmap.CompressFormat.PNG, 75, fOut) + fOut.flush() + fOut.close() + } catch (exception: Exception) { + return compressedBitmap + } + thumbnailCacheSize.addAndGet(cacheFile.length()) performEviction() return compressedBitmap } From 75b44f6f6f268758e9b9cf1785bc9444747e9be0 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 20 May 2019 21:31:30 +0100 Subject: [PATCH 016/134] [GDrive] Fixing crash issues from request limit exceeding --- app/build.gradle | 4 +- build.gradle | 4 +- scarlet/build.gradle | 18 ++-- .../1.json | 84 +++++++++++++++++++ .../quicknote/drive/GDriveActivitySpecs.kt | 1 - .../quicknote/drive/GDriveRemoteDatabase.kt | 16 ++-- .../quicknote/drive/GDriveServiceHelper.kt | 61 ++++++++++---- 7 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/1.json diff --git a/app/build.gradle b/app/build.gradle index a3add217..0f4e4fb8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 125 - versionName '6.9.7' + versionCode 126 + versionName '6.10.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/build.gradle b/build.gradle index a684175b..750045d4 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 125 - ext.appconfig_version = '6.9.7' + ext.appconfig_version_code = 126 + ext.appconfig_version = '6.10.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/build.gradle b/scarlet/build.gradle index c7e9428d..6c073925 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -15,6 +15,12 @@ android { targetSdkVersion rootProject.ext.appconfig_target_os_version versionCode rootProject.ext.appconfig_version_code versionName rootProject.ext.appconfig_version + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } flavorDimensions "tier" @@ -58,17 +64,19 @@ dependencies { implementation 'com.android.volley:volley:1.1.0' - implementation "com.google.firebase:firebase-core:16.0.7" - implementation "com.google.firebase:firebase-auth:16.1.0" - implementation "com.google.firebase:firebase-database:16.0.6" + implementation "com.google.firebase:firebase-core:16.0.9" + implementation "com.google.firebase:firebase-auth:17.0.0" + implementation "com.google.firebase:firebase-database:17.0.0" def litho_version = "0.21.0" compileOnly "com.facebook.litho:litho-annotations:$litho_version" kapt "com.facebook.litho:litho-processor:$litho_version" implementation 'com.google.android.gms:play-services-auth:16.0.1' - implementation 'com.google.http-client:google-http-client-gson:1.26.0' - implementation('com.google.api-client:google-api-client-android:1.26.0') { + implementation ('com.google.http-client:google-http-client-gson:1.26.0') { + + } + implementation ('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' } implementation('com.google.apis:google-api-services-drive:v3-rev136-1.25.0') { diff --git a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/1.json b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/1.json new file mode 100644 index 00000000..797a4cff --- /dev/null +++ b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/1.json @@ -0,0 +1,84 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "b1ba85f14c2fe791ecd49daf06616ef5", + "entities": [ + { + "tableName": "gdrive_upload", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdateTimestamp", + "columnName": "lastUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localStateDeleted", + "columnName": "localStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gDriveUpdateTimestamp", + "columnName": "gDriveUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gDriveStateDeleted", + "columnName": "gDriveStateDeleted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_gdrive_upload_uuid", + "unique": false, + "columnNames": [ + "uuid" + ], + "createSql": "CREATE INDEX `index_gdrive_upload_uuid` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b1ba85f14c2fe791ecd49daf06616ef5\")" + ] + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index f66214cd..58b3e5e2 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -21,7 +21,6 @@ object GDriveRootViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop loggingIn: Boolean): Component { - val activity = context.androidContext as CreateNoteActivity val buttonTitle = when { loggingIn -> R.string.google_drive_page_logging_in_button else -> R.string.google_drive_page_login_button diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index ca6c6c4f..27b1ebf4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -35,17 +35,17 @@ const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" var sGDriveFirstSyncNote: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) ?: Unit + get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) ?: false + set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) ?: Unit var sGDriveFirstSyncTag: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) ?: Unit + get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) ?: false + set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) ?: Unit var sGDriveFirstSyncFolder: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) ?: Unit + get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) ?: false + set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) ?: Unit var sGDriveFirstSyncImage: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) ?: Unit + get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) ?: false + set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) ?: Unit const val KEY_G_DRIVE_LAST_SYNC_DELTA_MS = 1000 * 60 const val KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC = "g_drive_first_time_sync_last_sync" diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 712d4012..3d61ff45 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -1,6 +1,8 @@ package com.bijoysingh.quicknote.drive +import android.os.SystemClock import android.util.Log +import com.bijoysingh.quicknote.BuildConfig import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.api.client.http.ByteArrayContent @@ -16,6 +18,7 @@ import java.io.InputStreamReader import java.util.* import java.util.concurrent.Callable import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicLong const val GOOGLE_DRIVE_ROOT_FOLDER = "Scarlet (App Data)" @@ -25,13 +28,41 @@ const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" -class ErrorCallable(val callable: Callable) : Callable { - override fun call(): T { +var lastCheckpointTime: AtomicLong = AtomicLong(0L) +var numQueriesSinceLastCheckpoint: AtomicLong = AtomicLong(0L) + +class ErrorCallable(val callable: Callable) : Callable { + override fun call(): T? { + val lastCheckpoint = lastCheckpointTime.get() + if (lastCheckpoint == 0L) { + lastCheckpointTime.set(System.currentTimeMillis()) + } + + val currentCount = numQueriesSinceLastCheckpoint.addAndGet(1) * 1.0 + val deltaTimeS = (System.currentTimeMillis() - lastCheckpointTime.get()) / 1000.0 + log("GDrive", "Request being called: currentCount=$currentCount, deltaTimeS=$deltaTimeS") + if (currentCount >= 10 && deltaTimeS > 0) { + when { + (currentCount / deltaTimeS) > 0.8 -> { + log("GDrive", "Rate limiting measures taken: currentCount=$currentCount, deltaTimeS=$deltaTimeS") + SystemClock.sleep(500L) + return call() + } + (currentCount / deltaTimeS) < 0.1 -> { + numQueriesSinceLastCheckpoint.set(1L) + lastCheckpointTime.set(SystemClock.currentThreadTimeMillis()) + } + } + } + try { return callable.call() } catch (exception: Exception) { - Log.e("GoogleDrive", exception.message, exception) - throw exception + Log.e("GDrive", exception.message, exception) + if (BuildConfig.DEBUG) { + throw exception + } + return null } } } @@ -48,11 +79,11 @@ fun getTrueCurrentTime(): Long { class GDriveServiceHelper(private val mDriveService: Drive) { private val mExecutor = Executors.newFixedThreadPool(8) - fun execute(callable: Callable): Task { + fun execute(callable: Callable): Task { return Tasks.call(mExecutor, ErrorCallable(callable)) } - fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { + fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { log("GDrive", "createFileWithData($folderId, $name)") val contentToSave = if (content.isEmpty()) updateTime.toString() else content return execute(Callable { @@ -66,7 +97,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { + fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { log("GDrive", "createFileWithData($folderId, $name, ${file.absolutePath})") return execute(Callable { val metadata = File() @@ -79,7 +110,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun createFolder(parentUid: String, folderName: String): Task { + fun createFolder(parentUid: String, folderName: String): Task { log("GDrive", "createFolder($parentUid, $folderName)") return execute(Callable { val metadata = File() @@ -93,7 +124,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun readFile(fileId: String): Task { + fun readFile(fileId: String): Task { log("GDrive", "readFile($fileId)") return execute(Callable { mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> @@ -111,7 +142,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun readFile(fileId: String, destinationFile: java.io.File): Task { + fun readFile(fileId: String, destinationFile: java.io.File): Task { log("GDrive", "readFile($fileId, ${destinationFile.absolutePath})") return execute(Callable { destinationFile.parentFile.mkdirs() @@ -121,7 +152,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { + fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { log("GDrive", "saveFile($fileId, $name)") return execute(Callable { val metadata = File().setModifiedTime(DateTime(updateTime)).setName(name) @@ -130,7 +161,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { + fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { log("GDrive", "getFilesInFolder($parentUid, $mimeType)") return execute(Callable { mDriveService.files().list() @@ -142,7 +173,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun getFolderQuery(parentUid: String, name: String): Task { + fun getFolderQuery(parentUid: String, name: String): Task { log("GDrive", "getFolderQuery($parentUid, $name)") val query = when { parentUid.isEmpty() -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name'" @@ -156,7 +187,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun getSubRootFolders(parentUid: String, names: List): Task { + fun getSubRootFolders(parentUid: String, names: List): Task { log("GDrive", "getSubRootFolders($parentUid, $names)") var nameQueryBuilder = "name = '${names[0]}'" names.subList(1, names.lastIndex + 1).forEach { @@ -170,7 +201,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun removeFileOrFolder(fileUid: String): Task { + fun removeFileOrFolder(fileUid: String): Task { log("GDrive", "removeFileOrFolder($fileUid)") return execute(Callable { mDriveService.files().delete(fileUid) From fa77b32f6fa9af658f5d26e7c0711a859ff26b75 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 21 May 2019 20:21:55 +0100 Subject: [PATCH 017/134] [GDrive] Improving GDrive Login Activity --- app/build.gradle | 4 ++-- .../base/main/sheets/WhatsNewBottomSheet.kt | 23 +++++++------------ .../base/support/sheets/LithoBottomSheet.kt | 2 +- .../base/support/specs/RoundIconSpec.kt | 3 ++- base/src/main/res/values/strings.xml | 1 + build.gradle | 4 ++-- scarlet/build.gradle | 2 +- .../quicknote/drive/GDriveActivitySpecs.kt | 12 ++++++---- .../quicknote/drive/GDriveServiceHelper.kt | 9 ++++---- .../firebase/support/ScarletAuthenticator.kt | 18 +++++++++------ 10 files changed, 40 insertions(+), 38 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0f4e4fb8..956e95d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 126 - versionName '6.10.0' + versionCode 127 + versionName '6.11.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index eb233beb..3a9175a0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -46,13 +46,13 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) - .text(WHATS_NEW_DETAILS_LAST_RELEASE_TITLE) + .text(WHATS_NEW_DETAILS_COMING_SOON_TITLE) .typeface(FONT_MONSERRAT) .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) - .text(Markdown.render(WHATS_NEW_DETAILS_LAST_RELEASE_MD, true)) + .text(Markdown.render(WHATS_NEW_DETAILS_COMING_SOON_MD, true)) .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.import_export_layout_exporting_done) @@ -70,25 +70,18 @@ class WhatsNewBottomSheet : LithoBottomSheet() { } companion object { - val WHATS_NEW_UID = 9 + val WHATS_NEW_UID = 10 val GOOGLE_TRANSLATE_URL = "https://translate.google.com/#auto/" val WHATS_NEW_DETAILS_SUBTITLE = "A lot has changed in this update, here is a summary of those changes." val WHATS_NEW_DETAILS_NEW_FEATURES_TITLE = "New Features" - val WHATS_NEW_DETAILS_LAST_RELEASE_TITLE = "Last Release" + val WHATS_NEW_DETAILS_COMING_SOON_TITLE = "Coming Soon" val WHATS_NEW_DETAILS_NEW_FEATURES_MD = - "- **All New UI:** New Note and Settings UI. Cleaner, faster and built for easy use.\n\n" + - "- **Easier Editor:** Easier and faster ways to get markdown, and section options.\n\n" + - "- **Realtime Markdown:** When you type in markdown you get real time conversion and formatting.\n\n" + - "- **More Editor Options:** Head over to settings to get more control on the editor experience.\n\n" + - "- **More Themes:** Pro Users get more themes for the app, and the default dark theme is even darker now.\n\n" + - "- **Folder Sync:** Sync all your notes to an folderSync folder live along with images.\n\n" + - "- **Widget Options:** Widgets now show formatted text! You can also configure what notes to see in the widget.\n\n" + + "- **Bug Fixes:** This release fixes multiple crash issues throughout the application.\n\n" + "Even more little things which help you enjoy using this app everyday" - val WHATS_NEW_DETAILS_LAST_RELEASE_MD = - "- **New UI and Icon:** New Search and Top Actionbar UI and icon\n\n" + - "- **Widgets:** Added a new list of notes widget. Also fixed widget not updating bug.\n\n" + - "- **Reminder:** Improved reminders to be more reliable." + val WHATS_NEW_DETAILS_COMING_SOON_MD = + "- **Google Drive Based Sync:** We are building a secure and private Google Drive based sync\n\n" + + "- **Photo Sync:** Drive sync will also allow syncing photos between devices as well.\n\n" } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 8e485be0..7e0efc8c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -31,7 +31,7 @@ fun openSheet(activity: AppCompatActivity, sheet: LithoBottomSheet) { fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { return Text.create(context) .textSizeRes(R.dimen.font_size_xxxlarge) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD) .marginDip(YogaEdge.HORIZONTAL, 20f) .marginDip(YogaEdge.TOP, 18f) .marginDip(YogaEdge.BOTTOM, 8f) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt index 5bba0317..fba1fe47 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.support.specs +import android.graphics.Color import android.graphics.drawable.Drawable import com.facebook.litho.ClickEvent import com.facebook.litho.Column @@ -35,7 +36,7 @@ object RoundIconSpec { .marginPx(YogaEdge.HORIZONTAL, iconMarginHorizontal ?: 0) .drawable(icon.color(iconColor)) .alpha(iconAlpha ?: 1f) - .background(LithoCircleDrawable(bgColor, bgAlpha ?: 255, showBorder ?: false)) + .background(LithoCircleDrawable(bgColor, bgAlpha ?: Color.alpha(bgColor), showBorder ?: false)) if (isClickDisabled === null || !isClickDisabled) { image.clickHandler(RoundIcon.onClickEvent(context)) } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 61aefd2d..de81190a 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -438,4 +438,5 @@ Signing In… Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. + Your photos are uploaded and synced across devices as well. diff --git a/build.gradle b/build.gradle index 750045d4..f65b2b88 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 126 - ext.appconfig_version = '6.10.0' + ext.appconfig_version_code = 127 + ext.appconfig_version = '6.11.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/build.gradle b/scarlet/build.gradle index 6c073925..f39c9548 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation ('com.google.http-client:google-http-client-gson:1.26.0') { - + exclude group: 'org.apache.httpcomponents' } implementation ('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index 58b3e5e2..e6db9dfc 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -1,5 +1,6 @@ package com.bijoysingh.quicknote.drive +import android.graphics.Color import android.graphics.drawable.Drawable import android.text.Layout import com.facebook.litho.* @@ -13,6 +14,7 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity +import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.LithoCircleDrawable import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -33,7 +35,7 @@ object GDriveRootViewSpec { .childComponent(GDriveContentView.create(context))) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) .textRes(R.string.google_drive_page_login_firebase_button) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_MONSERRAT)) @@ -89,8 +91,8 @@ object GDriveContentViewSpec { .titleRes(R.string.google_drive_page_login_lock_details)) .child(GDriveIconView.create(context) .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_action_lock) - .titleRes(R.string.google_drive_page_login_lock_details)) + .iconRes(R.drawable.ic_image_gallery) + .titleRes(R.string.google_drive_page_photo_details)) .build() } } @@ -106,8 +108,8 @@ object GDriveIconViewSpec { .paddingDip(YogaEdge.HORIZONTAL, 32f) .paddingDip(YogaEdge.VERTICAL, 24f) .child(Image.create(context) - .drawable(icon) - .background(LithoCircleDrawable(bgColor)) + .drawable(icon.color(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) + .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) .paddingDip(YogaEdge.ALL, 12f) .marginDip(YogaEdge.BOTTOM, 12f) .heightDip(64f)) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 3d61ff45..ce6946ac 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -38,24 +38,25 @@ class ErrorCallable(val callable: Callable) : Callable { lastCheckpointTime.set(System.currentTimeMillis()) } - val currentCount = numQueriesSinceLastCheckpoint.addAndGet(1) * 1.0 + val currentCount = numQueriesSinceLastCheckpoint.get() * 1.0 val deltaTimeS = (System.currentTimeMillis() - lastCheckpointTime.get()) / 1000.0 - log("GDrive", "Request being called: currentCount=$currentCount, deltaTimeS=$deltaTimeS") + log("GDrive", "Request being called: currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=${currentCount/deltaTimeS}") if (currentCount >= 10 && deltaTimeS > 0) { when { - (currentCount / deltaTimeS) > 0.8 -> { + (currentCount / deltaTimeS) > 0.9 -> { log("GDrive", "Rate limiting measures taken: currentCount=$currentCount, deltaTimeS=$deltaTimeS") SystemClock.sleep(500L) return call() } (currentCount / deltaTimeS) < 0.1 -> { - numQueriesSinceLastCheckpoint.set(1L) + numQueriesSinceLastCheckpoint.set(0L) lastCheckpointTime.set(SystemClock.currentThreadTimeMillis()) } } } try { + numQueriesSinceLastCheckpoint.addAndGet(1) return callable.call() } catch (exception: Exception) { Log.e("GDrive", exception.message, exception) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index 14e486e1..1629762a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -32,14 +32,17 @@ class ScarletAuthenticator() : IAuthenticator { } override fun setup(context: Context) { - GlobalScope.launch { - val account = GoogleSignIn.getLastSignedInAccount(context) - if (account !== null) { - val helper= GDriveLoginActivity.getDriveHelper(context, account) - gDrive = GDriveRemoteDatabase(WeakReference(context)) - gDrive?.init(helper) + /* + // TODO: Uncomment this when GDrive is ready + GlobalScope.launch { + val account = GoogleSignIn.getLastSignedInAccount(context) + if (account !== null) { + val helper= GDriveLoginActivity.getDriveHelper(context, account) + gDrive = GDriveRemoteDatabase(WeakReference(context)) + gDrive?.init(helper) + } } - } + */ GlobalScope.launch { FirebaseApp.initializeApp(context) @@ -68,6 +71,7 @@ class ScarletAuthenticator() : IAuthenticator { } override fun openLoginActivity(context: Context) = Runnable { + // TODO: Uncomment this when GDrive is ready // context.startActivity(Intent(context, GDriveLoginActivity::class.java)) context.startActivity(Intent(context, LoginActivity::class.java)) } From 925ca0a3280ebde68b1f83e9c84bd4ebbec11c06 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 21 May 2019 21:04:58 +0100 Subject: [PATCH 018/134] Adding option to disable sorting checked items --- app/build.gradle | 4 ++-- .../maubis/scarlet/base/core/format/Format.kt | 8 +++++++- .../base/main/sheets/WhatsNewBottomSheet.kt | 1 + .../creation/sheet/EditorOptionsBottomSheet.kt | 16 ++++++++++++++++ base/src/main/res/values/strings.xml | 3 +++ build.gradle | 4 ++-- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 956e95d8..54f72102 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 127 - versionName '6.11.0' + versionCode 128 + versionName '6.12.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt index 9b140fd1..c9f18f7f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.core.format +import com.maubis.scarlet.base.note.creation.sheet.sEditorMoveChecked import org.json.JSONObject import java.util.* @@ -62,13 +63,18 @@ class Format { } fun sectionPreservingSort(formats: List): List { + if (!sEditorMoveChecked) { + return formats + } + val mutableFormats = formats.toMutableList() var index = 0 while (index < formats.size - 1) { val currentItem = mutableFormats[index] val nextItem = mutableFormats[index + 1] - if (currentItem.formatType == FormatType.CHECKLIST_CHECKED && nextItem.formatType == FormatType.CHECKLIST_UNCHECKED) { + if (currentItem.formatType == FormatType.CHECKLIST_CHECKED + && nextItem.formatType == FormatType.CHECKLIST_UNCHECKED) { Collections.swap(mutableFormats, index, index + 1) continue } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 3a9175a0..eab9b78b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -77,6 +77,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { val WHATS_NEW_DETAILS_NEW_FEATURES_TITLE = "New Features" val WHATS_NEW_DETAILS_COMING_SOON_TITLE = "Coming Soon" val WHATS_NEW_DETAILS_NEW_FEATURES_MD = + "- **Checked Items:** You can now enable / disable checked items from moving down when checked.\n\n" + "- **Bug Fixes:** This release fixes multiple crash issues throughout the application.\n\n" + "Even more little things which help you enjoy using this app everyday" val WHATS_NEW_DETAILS_COMING_SOON_MD = diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt index e055ba81..fca132f9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt @@ -9,6 +9,7 @@ import com.maubis.scarlet.base.support.sheets.LithoOptionsItem const val STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED = "KEY_MARKDOWN_ENABLED" const val STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN = "editor_live_markdown" +const val STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS = "editor_move_checked_items" const val STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT = "editor_markdown_default" const val STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES = "editor_move_handles" @@ -16,6 +17,10 @@ var sEditorLiveMarkdown: Boolean get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, true) set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, value) +var sEditorMoveChecked: Boolean + get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS, value) + var sEditorMarkdownDefault: Boolean get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, false) set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, value) @@ -67,6 +72,17 @@ class EditorOptionsBottomSheet : LithoOptionBottomSheet() { reset(componentContext.androidContext, dialog) } )) + items.add(LithoOptionsItem( + title = R.string.editor_option_move_checked_items, + subtitle = R.string.editor_option_move_checked_items_description, + icon = R.drawable.ic_check_box_white_24dp, + selected = sEditorMoveChecked, + isSelectable = true, + listener = { + sEditorMoveChecked = !sEditorMoveChecked + reset(componentContext.androidContext, dialog) + } + )) items.add(LithoOptionsItem( title = R.string.editor_option_enable_move_handle, subtitle = R.string.editor_option_enable_move_handle_description, diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index de81190a..e8ff71e3 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -401,6 +401,9 @@ Realtime Markdown Choose if you want markdown to be visible as your type. Enabling can effect performance on large notes. + Move Checked Items + Checked items move to the bottom of the list. Uncheck does not reset position. + Show Movement Handles Show the handle to move items up or down. You can still move things around by touching the corner. diff --git a/build.gradle b/build.gradle index f65b2b88..9d5ef1ee 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 127 - ext.appconfig_version = '6.11.0' + ext.appconfig_version_code = 128 + ext.appconfig_version = '6.12.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From 549b9e32b630376b0d06c9917489341f48c9673d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 21 May 2019 21:28:50 +0100 Subject: [PATCH 019/134] Moving FAQ to internal settings --- .../sheet/AboutSettingsOptionsBottomSheet.kt | 12 ++++++++++++ .../settings/sheet/SettingsOptionsBottomSheet.kt | 12 ------------ faq/README.md | 5 ++++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt index 62c700e8..2b72ce57 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt @@ -40,6 +40,18 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { dismiss() } )) + options.add(LithoOptionsItem( + title = R.string.home_option_faq_title, + subtitle = R.string.home_option_faq_description, + icon = R.drawable.icon_help, + listener = { + try { + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SettingsOptionsBottomSheet.GITHUB_FAQ_URL))) + dismiss() + } catch (exception: Exception) { + } + } + )) options.add(LithoOptionsItem( title = R.string.material_notes_privacy_policy, subtitle = R.string.material_notes_privacy_policy_subtitle, diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index ad8d6918..7e123af4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -127,18 +127,6 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { openSheet(activity, DeleteAndMoreOptionsBottomSheet()) } )) - options.add(LithoOptionsItem( - title = R.string.home_option_faq_title, - subtitle = R.string.home_option_faq_description, - icon = R.drawable.icon_help, - listener = { - try { - activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_FAQ_URL))) - dismiss() - } catch (exception: Exception) { - } - } - )) options.add(LithoOptionsItem( title = R.string.home_option_logout_of_app, subtitle = R.string.home_option_logout_of_app_subtitle, diff --git a/faq/README.md b/faq/README.md index a059ac54..c4bbdcf2 100644 --- a/faq/README.md +++ b/faq/README.md @@ -1,2 +1,5 @@ # Frequently Asked Questions -> Test FAQ Page \ No newline at end of file +It is a curated list of questions asked by users. We plan to add more questions as you ask more questions. So if you did not find your question? Add a question [here](https://github.com/BijoySingh/Scarlet-Notes/issues) with `[FAQ]` in the title or, email us at team.thecodershub@gmail.com with `[FAQ]` in the title. + +**How to add notes?** +Simply press the "Add Note" button in the bottom right of the home screen. You can then start adding your notes in the new screen. \ No newline at end of file From 0df0aaa215ae5659b6e916674f51009ff7ee60d2 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 21 May 2019 23:10:30 +0100 Subject: [PATCH 020/134] Upgrading the home option sheet to new UI --- .../main/sheets/HomeNavigationBottomSheet.kt | 207 --------------- .../main/sheets/HomeOptionsBottomSheet.kt | 241 ++++++++++++++++++ .../main/specs/MainActivityBottomBarSpec.kt | 5 +- .../base/support/sheets/LithoBottomSheet.kt | 11 +- .../support/sheets/LithoOptionBottomSheet.kt | 46 ++++ 5 files changed, 299 insertions(+), 211 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt deleted file mode 100644 index 60eb48d8..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeNavigationBottomSheet.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.maubis.scarlet.base.main.sheets - -import android.app.Dialog -import android.support.v4.content.ContextCompat -import android.view.View -import android.widget.LinearLayout -import android.widget.TextView -import com.github.bijoysingh.starter.util.LocaleManager -import com.github.bijoysingh.uibasics.views.UITextView -import com.maubis.scarlet.base.MainActivity -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb -import com.maubis.scarlet.base.core.tag.TagBuilder -import com.maubis.scarlet.base.main.HomeNavigationState -import com.maubis.scarlet.base.note.tag.TagOptionsItem -import com.maubis.scarlet.base.note.tag.sheet.CreateOrEditTagBottomSheet -import com.maubis.scarlet.base.note.tag.view.HomeTagView -import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet -import com.maubis.scarlet.base.support.SearchConfig -import com.maubis.scarlet.base.support.option.OptionsItem -import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase -import com.maubis.scarlet.base.support.ui.Theme -import com.maubis.scarlet.base.support.ui.ThemeColorType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch - -class HomeNavigationBottomSheet : GridBottomSheetBase() { - - override fun setupViewWithDialog(dialog: Dialog) { - resetOptions(dialog) - resetTags(dialog) - setAddTagOption(dialog) - makeBackgroundTransparent(dialog, R.id.root_layout) - } - - private fun getOptions(): List { - val activity = context as MainActivity - val options = ArrayList() - options.add(OptionsItem( - title = R.string.nav_home, - subtitle = R.string.nav_home_details, - icon = R.drawable.ic_home_white_48dp, - selected = activity.config.mode == HomeNavigationState.DEFAULT, - listener = View.OnClickListener { - activity.onHomeClick(); - dismiss(); - } - )) - options.add(OptionsItem( - title = R.string.nav_favourites, - subtitle = R.string.nav_favourites_details, - icon = R.drawable.ic_favorite_white_48dp, - selected = activity.config.mode == HomeNavigationState.FAVOURITE, - listener = View.OnClickListener { - activity.onFavouritesClick(); - dismiss(); - } - )) - options.add(OptionsItem( - title = R.string.nav_archived, - subtitle = R.string.nav_archived_details, - icon = R.drawable.ic_archive_white_48dp, - selected = activity.config.mode == HomeNavigationState.ARCHIVED, - listener = View.OnClickListener { - activity.onArchivedClick(); - dismiss(); - } - )) - options.add(OptionsItem( - title = R.string.nav_locked, - subtitle = R.string.nav_locked_details, - icon = R.drawable.ic_action_lock, - selected = activity.config.mode == HomeNavigationState.LOCKED, - listener = View.OnClickListener { - activity.onLockedClick(); - dismiss(); - } - )) - options.add(OptionsItem( - title = R.string.nav_trash, - subtitle = R.string.nav_trash_details, - icon = R.drawable.ic_delete_white_48dp, - selected = activity.config.mode == HomeNavigationState.TRASH, - listener = View.OnClickListener { - activity.onTrashClick(); - dismiss(); - } - )) - options.add(OptionsItem( - title = R.string.nav_settings, - subtitle = R.string.nav_settings, - icon = R.drawable.ic_action_settings, - listener = View.OnClickListener { - SettingsOptionsBottomSheet.openSheet(activity) - dismiss(); - } - )) - return options - } - - fun resetOptions(dialog: Dialog) { - GlobalScope.launch(Dispatchers.Main) { - val items = GlobalScope.async(Dispatchers.IO) { getOptions() } - setOptions(dialog, items.await()) - } - } - - fun resetTags(dialog: Dialog) { - GlobalScope.launch(Dispatchers.Main) { - val tags = GlobalScope.async(Dispatchers.IO) { getTagOptions() } - - val titleView = dialog.findViewById(R.id.tag_options_title) - titleView.setTextColor( - ApplicationBase.instance.themeController().get(themedContext(), - Theme.DARK, ThemeColorType.SECONDARY_TEXT)) - - val layout = dialog.findViewById(R.id.options_container) - layout.removeAllViews() - setTagOptions(dialog, tags.await()) - } - } - - private fun setTagOptions(dialog: Dialog, options: List) { - val layout = dialog.findViewById(R.id.options_container); - for (option in options.sorted()) { - val contentView = HomeTagView(View.inflate(context, R.layout.layout_home_tag_item, null)) - contentView.title.setText(option.tag.title) - contentView.rootView.setOnClickListener(option.listener) - contentView.subtitle.visibility = View.GONE - contentView.icon.setImageResource(option.getIcon()) - - contentView.action.setImageResource(option.getEditIcon()); - contentView.action.setColorFilter(ApplicationBase.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.HINT_TEXT)); - contentView.action.setOnClickListener(option.editListener) - - if (option.usages > 0) { - contentView.subtitle.setText(LocaleManager.toString(option.usages)) - contentView.subtitle.visibility = View.VISIBLE - } - - contentView.title.setTextColor(getOptionsTitleColor(option.selected)) - contentView.subtitle.setTextColor(getOptionsSubtitleColor(option.selected)) - contentView.icon.setColorFilter(getOptionsTitleColor(option.selected)) - - layout.addView(contentView.rootView) - } - } - - private fun getTagOptions(): List { - val activity = context as MainActivity - val options = ArrayList() - for (tag in tagsDb.getAll()) { - options.add(TagOptionsItem( - tag = tag, - usages = notesDb.getNoteCountByTag(tag.uuid), - listener = View.OnClickListener { - activity.config = SearchConfig(mode = HomeNavigationState.DEFAULT) - activity.openTag(tag) - dismiss() - }, - editable = true, - editListener = View.OnClickListener { - CreateOrEditTagBottomSheet.openSheet(activity, tag, { _, _ -> resetTags(dialog) }) - } - )) - } - return options - } - - private fun setAddTagOption(dialog: Dialog) { - val hintTextColor = ApplicationBase.instance.themeController().get(themedContext(), Theme.DARK, ThemeColorType.HINT_TEXT) - val newTagButton = dialog.findViewById(R.id.new_tag_button) - newTagButton.setTextColor(hintTextColor) - newTagButton.setImageTint(hintTextColor) - newTagButton.setOnClickListener { onNewTagClick() } - newTagButton.icon.alpha = 0.6f - } - - private fun onNewTagClick() { - val activity = context as MainActivity - CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag(), { _, _ -> resetTags(dialog) }) - } - - override fun getOptionsTitleColor(selected: Boolean): Int { - return ContextCompat.getColor(themedContext(), R.color.light_primary_text) - } - - override fun getOptionsSubtitleColor(selected: Boolean): Int { - return ContextCompat.getColor(themedContext(), R.color.light_secondary_text) - } - - override fun getBackgroundCardViewIds(): Array = emptyArray() - - override fun getLayout(): Int = R.layout.bottom_sheet_home_navigation - - companion object { - fun openSheet(activity: MainActivity) { - val sheet = HomeNavigationBottomSheet() - - sheet.show(activity.supportFragmentManager, sheet.tag) - } - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt new file mode 100644 index 00000000..d178fd47 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt @@ -0,0 +1,241 @@ +package com.maubis.scarlet.base.main.sheets + +import android.app.Dialog +import android.graphics.Typeface +import com.facebook.litho.* +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.widget.Text +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.main.HomeNavigationState +import com.maubis.scarlet.base.note.tag.sheet.CreateOrEditTagBottomSheet +import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet +import com.maubis.scarlet.base.support.SearchConfig +import com.maubis.scarlet.base.support.sheets.* +import com.maubis.scarlet.base.support.specs.RoundIcon +import com.maubis.scarlet.base.support.ui.ThemeColorType + +class LithoTagOptionsItem( + val tag: Tag, + val usages: Int = 0, + val isSelected: Boolean = false, + val isEditable: Boolean = false, + val editListener: () -> Unit, + val listener: () -> Unit) { +} + +@LayoutSpec +object TagItemLayoutSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, @Prop option: LithoTagOptionsItem): Component { + val theme = ApplicationBase.instance.themeController() + val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) + val selectedColor = theme.get(ThemeColorType.ACCENT_TEXT) + + val icon: Int + val bgColor: Int + val bgAlpha: Int + when (option.isSelected) { + true -> { + icon = R.drawable.ic_action_label + bgColor = selectedColor + bgAlpha = 200 + } + false -> { + icon = R.drawable.ic_action_label_unselected + bgColor = titleColor + bgAlpha = 15 + } + } + + val row = Row.create(context) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + RoundIcon.create(context) + .iconRes(icon) + .bgColor(bgColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(bgAlpha) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.END, 16f)) + .child(Text.create(context) + .flexGrow(1f) + .text(option.tag.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(CoreConfig.FONT_MONSERRAT) + .textStyle(Typeface.BOLD) + .textColor(titleColor)) + + if (option.usages > 0) { + row.child(Text.create(context) + .text("${option.usages}") + .textSizeRes(R.dimen.font_size_normal) + .textColor(titleColor) + .marginDip(YogaEdge.HORIZONTAL, 8f)) + } + + if (option.isEditable) { + row.child(RoundIcon.create(context) + .iconRes(R.drawable.ic_edit_white_48dp) + .bgColor(titleColor) + .bgAlpha(15) + .iconAlpha(0.9f) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .onClick { option.editListener() } + .isClickDisabled(false) + .marginDip(YogaEdge.START, 12f)) + } + + row.clickHandler(OptionItemLayout.onItemClick(context)) + return row.build() + } + + @OnEvent(ClickEvent::class) + fun onItemClick(context: ComponentContext, @Prop option: LithoTagOptionsItem) { + option.listener() + } +} + + +class HomeOptionsBottomSheet : LithoBottomSheet() { + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + val activity = context as MainActivity + val options = getOptions() + val component = Column.create(componentContext) + .widthPercent(100f) + .child(Column.create(componentContext) + .paddingDip(YogaEdge.TOP, 20f) + .paddingDip(YogaEdge.BOTTOM, 20f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + Row.create(componentContext) + .child(OptionLabelItemLayout.create(componentContext).option(options[0]).onClick { options[0].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[1]).onClick { options[1].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[2]).onClick { options[2].listener() }) + ) + .child( + Row.create(componentContext) + .child(OptionLabelItemLayout.create(componentContext).option(options[3]).onClick { options[3].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[4]).onClick { options[4].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[5]).onClick { options[5].listener() }) + )) + + val tagsComponent = Column.create(componentContext) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 20f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .backgroundRes(R.color.dark_hint_text) + .child(getLithoBottomSheetTitle(componentContext) + .textRes(R.string.tag_sheet_choose_tag) + .marginDip(YogaEdge.BOTTOM, 12f)) + getTagOptions().forEach { + tagsComponent.child(TagItemLayout.create(componentContext).option(it)) + } + + val addTag = LithoOptionsItem( + title = R.string.tag_sheet_new_tag_button, + subtitle = 0, + icon = R.drawable.icon_add_note, + listener = { CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { _, _ -> reset(activity, dialog) } }) + tagsComponent.child(OptionItemLayout.create(componentContext).option(addTag).onClick { addTag.listener() }) + + component.child(tagsComponent) + return component.build() + } + + override fun bottomMargin(): Float = 0f + + private fun getOptions(): List { + val activity = context as MainActivity + val options = ArrayList() + options.add(LithoLabelOptionsItem( + title = R.string.nav_home, + icon = R.drawable.ic_home_white_48dp, + listener = { + activity.onHomeClick() + dismiss() + } + )) + options.add(LithoLabelOptionsItem( + title = R.string.nav_favourites, + icon = R.drawable.ic_favorite_white_48dp, + listener = { + activity.onFavouritesClick(); + dismiss(); + } + )) + options.add(LithoLabelOptionsItem( + title = R.string.nav_archived, + icon = R.drawable.ic_archive_white_48dp, + listener = { + activity.onArchivedClick(); + dismiss(); + } + )) + options.add(LithoLabelOptionsItem( + title = R.string.nav_locked, + icon = R.drawable.ic_action_lock, + listener = { + activity.onLockedClick(); + dismiss(); + } + )) + options.add(LithoLabelOptionsItem( + title = R.string.nav_trash, + icon = R.drawable.ic_delete_white_48dp, + listener = { + activity.onTrashClick(); + dismiss(); + } + )) + options.add(LithoLabelOptionsItem( + title = R.string.nav_settings, + icon = R.drawable.ic_action_settings, + listener = { + SettingsOptionsBottomSheet.openSheet(activity) + dismiss(); + } + )) + return options + } + + private fun getTagOptions(): List { + val activity = context as MainActivity + val options = ArrayList() + for (tag in CoreConfig.tagsDb.getAll()) { + options.add(LithoTagOptionsItem( + tag = tag, + usages = CoreConfig.notesDb.getNoteCountByTag(tag.uuid), + listener = { + activity.config = SearchConfig(mode = HomeNavigationState.DEFAULT) + activity.openTag(tag) + dismiss() + }, + isEditable = true, + isSelected = false, + editListener = { + CreateOrEditTagBottomSheet.openSheet(activity, tag) { _, _ -> reset(activity, dialog) } + } + )) + } + return options + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 4f51b36f..a219fa6b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -16,10 +16,11 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.main.sheets.HomeNavigationBottomSheet +import com.maubis.scarlet.base.main.sheets.HomeOptionsBottomSheet import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.folder.sheet.CreateOrEditFolderBottomSheet import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.specs.bottomBarCard @@ -43,7 +44,7 @@ object MainActivityBottomBarSpec { .bgColor(Color.TRANSPARENT) .iconRes(R.drawable.ic_apps_white_48dp) .onClick { - HomeNavigationBottomSheet.openSheet(activity) + openSheet(activity, HomeOptionsBottomSheet()) }) row.child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 7e0efc8c..40ac7016 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -88,10 +88,11 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { } val baseComponent = Column.create(componentContext) - .paddingDip(YogaEdge.VERTICAL, 16f) + .paddingDip(YogaEdge.TOP, topMargin()) + .paddingDip(YogaEdge.BOTTOM, bottomMargin()) .widthPercent(100f) .alignItems(YogaAlign.CENTER) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(backgroundColor(componentContext)) .child( Image.create(componentContext) .drawableRes(topHandle) @@ -116,4 +117,10 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { } abstract fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component + + open fun backgroundColor(componentContext: ComponentContext) = ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND) + + open fun topMargin() = 16f + + open fun bottomMargin() = 16f } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt index 1c722ff1..ec358bf6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt @@ -112,6 +112,52 @@ object OptionItemLayoutSpec { } } +class LithoLabelOptionsItem( + val title: Int, + val icon: Int, + val visible: Boolean = true, + val listener: () -> Unit) + +@LayoutSpec +object OptionLabelItemLayoutSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop option: LithoLabelOptionsItem): Component { + val theme = ApplicationBase.instance.themeController() + val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) + + val row = Column.create(context) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 16f) + .child( + RoundIcon.create(context) + .iconRes(option.icon) + .bgColor(titleColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(15) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.BOTTOM, 4f)) + .child(Text.create(context) + .textRes(option.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(FONT_MONSERRAT) + .textStyle(BOLD) + .textColor(titleColor)) + row.clickHandler(OptionItemLayout.onItemClick(context)) + return row.build() + } + + @OnEvent(ClickEvent::class) + fun onItemClick(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + + abstract class LithoOptionBottomSheet : LithoBottomSheet() { abstract fun title(): Int From 60269241f352a6dbdc5400ecd530f35b5feb3c3d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 21 May 2019 23:28:49 +0100 Subject: [PATCH 021/134] Updating FAQ --- faq/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/faq/README.md b/faq/README.md index c4bbdcf2..8e1dfc70 100644 --- a/faq/README.md +++ b/faq/README.md @@ -1,5 +1,5 @@ # Frequently Asked Questions It is a curated list of questions asked by users. We plan to add more questions as you ask more questions. So if you did not find your question? Add a question [here](https://github.com/BijoySingh/Scarlet-Notes/issues) with `[FAQ]` in the title or, email us at team.thecodershub@gmail.com with `[FAQ]` in the title. -**How to add notes?** +## How to add notes? Simply press the "Add Note" button in the bottom right of the home screen. You can then start adding your notes in the new screen. \ No newline at end of file From 61b0580df2ba3ad195d21a0fca78f4a83d508c74 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 22 May 2019 00:18:06 +0100 Subject: [PATCH 022/134] Adding option to escape markdown, and disabling markdown in code --- app/build.gradle | 4 ++-- .../formats/recycler/FormatViewHolderBase.kt | 2 +- build.gradle | 4 ++-- .../inliners/InlineMultipleTextTests.kt | 10 +++++++++ .../segments/SegmentSimpleMultiLineTests.kt | 17 +++++++++++++++ .../markdown/inliner/MarkdownInlineType.kt | 1 + .../markdown/inliner/MarkdownInlines.kt | 5 +++-- .../markdown/inliner/TextInlineConfig.kt | 21 +++++++++++++++++++ .../maubis/markdown/inliner/TextInliner.kt | 9 ++++++++ .../com/maubis/markdown/spannable/SpanInfo.kt | 1 + 10 files changed, 67 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 54f72102..b4eab6ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 128 - versionName '6.12.0' + versionCode 129 + versionName '6.13.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index 82acffd1..e6eb8bab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -75,7 +75,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView && !extra.getBoolean(KEY_EDITABLE)), isMarkdownEnabled = (extra == null || extra.getBoolean(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) - || data.forcedMarkdown), + || data.forcedMarkdown) && (data.formatType != FormatType.CODE), fontSize = { val fontSize = extra?.getInt(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) ?: TEXT_SIZE_DEFAULT diff --git a/build.gradle b/build.gradle index 9d5ef1ee..87c3f617 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 128 - ext.appconfig_version = '6.12.0' + ext.appconfig_version_code = 129 + ext.appconfig_version = '6.13.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineMultipleTextTests.kt b/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineMultipleTextTests.kt index bbbae4bb..a3388419 100644 --- a/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineMultipleTextTests.kt +++ b/markdown/src/androidTest/java/com/maubis/markdown/inliners/InlineMultipleTextTests.kt @@ -37,4 +37,14 @@ class InlineMultipleTextTests : MarkdownTextInlinerTestBase() { PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.STRIKE), listOf(NormalInlineMarkdownSegment("t5"))), PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INLINE_CODE), listOf(NormalInlineMarkdownSegment("t6"))))), processed) } + + @Test + fun testEscapedText() { + val textA = "aaa\\_bb_c" + val processedA = TextInliner(textA).get() + assert(PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.INVALID), listOf( + NormalInlineMarkdownSegment("aaa"), + PhraseDelimiterMarkdownInline(InvalidInline(MarkdownInlineType.IGNORE_CHAR), listOf(NormalInlineMarkdownSegment("_"))), + NormalInlineMarkdownSegment("bb_c"))), processedA) + } } diff --git a/markdown/src/androidTest/java/com/maubis/markdown/segments/SegmentSimpleMultiLineTests.kt b/markdown/src/androidTest/java/com/maubis/markdown/segments/SegmentSimpleMultiLineTests.kt index a824c7bc..0506f6ff 100644 --- a/markdown/src/androidTest/java/com/maubis/markdown/segments/SegmentSimpleMultiLineTests.kt +++ b/markdown/src/androidTest/java/com/maubis/markdown/segments/SegmentSimpleMultiLineTests.kt @@ -120,4 +120,21 @@ class SegmentSimpleMultiLineTests : MarkdownTextSegmenterTestBase() { assert(text, processed) } + + + @Test + fun testMarkdownInsideCode() { + val text = "```\n" + + "**co** _d_ \\e\n" + + "```\n" + + "---\n" + + "## heading" + val processed = TextSegmenter(text).get() + assert(listOf( + getTestSegment(MarkdownSegmentType.CODE, "```\n**co** _d_ \\e\n```"), + getTestSegment(MarkdownSegmentType.SEPARATOR, "---"), + getTestSegment(MarkdownSegmentType.HEADING_2, "## heading")), processed) + assert(text, processed) + } + } diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlineType.kt b/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlineType.kt index 6b8d3c23..e6e14a87 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlineType.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlineType.kt @@ -8,4 +8,5 @@ enum class MarkdownInlineType { UNDERLINE, INLINE_CODE, STRIKE, + IGNORE_CHAR, } \ No newline at end of file diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlines.kt b/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlines.kt index bd93ff31..15b8ec6b 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlines.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/MarkdownInlines.kt @@ -107,8 +107,9 @@ class PhraseDelimiterMarkdownInline(val config: IInlineConfig, val children: Lis override fun toMarkwon(): String { val builder = StringBuilder() val config = TextInlineConfig.getDefaultOutputConfig(type()) - if (config is PhraseDelimiterInline) { - builder.append(config.startDelimiter) + when { + config is PhraseDelimiterInline -> builder.append(config.startDelimiter) + config is StartMarkerInline -> builder.append(config.startDelimiter) } children.forEach { builder.append(it.toMarkwon()) } if (config is PhraseDelimiterInline) { diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/TextInlineConfig.kt b/markdown/src/main/java/com/maubis/markdown/inliner/TextInlineConfig.kt index b916b45b..b8f7702b 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/TextInlineConfig.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/TextInlineConfig.kt @@ -13,6 +13,23 @@ class InvalidInline(val type: MarkdownInlineType) : IInlineConfig { override fun type() = type } +class StartMarkerInline( + val type: MarkdownInlineType, val startDelimiter: String) : IInlineConfig { + override fun type() = type + + override fun isStart(segment: String, index: Int): Boolean { + if (index + startDelimiter.length > segment.length) { + return false + } + return segment.regionMatches(index, startDelimiter, 0, startDelimiter.length, true) + } + + override fun isEnd(segment: String, index: Int): Boolean = true + + override fun startIncrement(): Int = startDelimiter.length + override fun identifier(): String = "${type().name}($startDelimiter)" +} + class PhraseDelimiterInline( val type: MarkdownInlineType, val startDelimiter: String, @@ -98,6 +115,9 @@ class TextInlineConfig(builder: Builder) { PhraseDelimiterInline(MarkdownInlineType.STRIKE, "~", "~"), PhraseDelimiterInline(MarkdownInlineType.STRIKE, "~~", "~~"), PhraseDelimiterInline(MarkdownInlineType.STRIKE, "~", "~")) + MarkdownInlineType.IGNORE_CHAR -> arrayOf( + StartMarkerInline(MarkdownInlineType.IGNORE_CHAR, "\\") + ) } } @@ -110,6 +130,7 @@ class TextInlineConfig(builder: Builder) { MarkdownInlineType.UNDERLINE -> PhraseDelimiterInline(MarkdownInlineType.UNDERLINE, "", "") MarkdownInlineType.INLINE_CODE -> PhraseDelimiterInline(MarkdownInlineType.INLINE_CODE, "`", "`") MarkdownInlineType.STRIKE -> PhraseDelimiterInline(MarkdownInlineType.STRIKE, "~~", "~~") + MarkdownInlineType.IGNORE_CHAR -> StartMarkerInline(MarkdownInlineType.IGNORE_CHAR, "\\") } } } diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt index 0c82e619..ff80f8cb 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt @@ -38,6 +38,15 @@ class TextInliner(val text: String) { continue } + if (currentInline.config.type() == MarkdownInlineType.IGNORE_CHAR) { + textSegment.builder.append(char) + addTextComponent() + currentInline.paired = true + index += 1 + unshelveSegment() + continue + } + if (currentInline.config.type() != MarkdownInlineType.INVALID && currentInline.config.isEnd(text, index)) { addTextComponent() diff --git a/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt b/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt index 8bdae613..0d9525ee 100644 --- a/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt +++ b/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt @@ -55,5 +55,6 @@ fun map(type: MarkdownInlineType): MarkdownType { MarkdownInlineType.UNDERLINE -> MarkdownType.UNDERLINE MarkdownInlineType.INLINE_CODE -> MarkdownType.INLINE_CODE MarkdownInlineType.STRIKE -> MarkdownType.STRIKE + MarkdownInlineType.IGNORE_CHAR -> MarkdownType.NORMAL } } \ No newline at end of file From 67f745113a2281547d30dd1b792fbbb8b17b606f Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 22 May 2019 21:29:55 +0100 Subject: [PATCH 023/134] Updating Note Tag Chooser Options UI --- .../main/sheets/HomeOptionsBottomSheet.kt | 20 +++-- .../scarlet/base/note/NoteExtensions.kt | 2 +- .../note/actions/NoteOptionsBottomSheet.kt | 10 +-- .../tag/sheet/TagChooseOptionsBottomSheet.kt | 73 --------------- .../note/tag/sheet/TagChooserBottomSheet.kt | 88 +++++++++++++++++++ .../layout/bottom_sheet_home_navigation.xml | 81 ----------------- 6 files changed, 109 insertions(+), 165 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt delete mode 100644 base/src/main/res/layout/bottom_sheet_home_navigation.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt index d178fd47..589af913 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt @@ -29,8 +29,8 @@ class LithoTagOptionsItem( val usages: Int = 0, val isSelected: Boolean = false, val isEditable: Boolean = false, - val editListener: () -> Unit, - val listener: () -> Unit) { + val editListener: () -> Unit = {}, + val listener: () -> Unit = {}) { } @LayoutSpec @@ -39,21 +39,30 @@ object TagItemLayoutSpec { fun onCreate(context: ComponentContext, @Prop option: LithoTagOptionsItem): Component { val theme = ApplicationBase.instance.themeController() val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) - val selectedColor = theme.get(ThemeColorType.ACCENT_TEXT) + val selectedColor = when (theme.isNightTheme()) { + true -> context.getColor(R.color.material_blue_400) + false -> context.getColor(R.color.material_blue_700) + } val icon: Int val bgColor: Int val bgAlpha: Int + val textColor: Int + val typeface: Typeface when (option.isSelected) { true -> { icon = R.drawable.ic_action_label bgColor = selectedColor bgAlpha = 200 + textColor = selectedColor + typeface = CoreConfig.FONT_MONSERRAT_MEDIUM } false -> { icon = R.drawable.ic_action_label_unselected bgColor = titleColor bgAlpha = 15 + textColor = titleColor + typeface = CoreConfig.FONT_MONSERRAT } } @@ -77,9 +86,9 @@ object TagItemLayoutSpec { .flexGrow(1f) .text(option.tag.title) .textSizeRes(R.dimen.font_size_normal) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(typeface) .textStyle(Typeface.BOLD) - .textColor(titleColor)) + .textColor(textColor)) if (option.usages > 0) { row.child(Text.create(context) @@ -236,6 +245,7 @@ class HomeOptionsBottomSheet : LithoBottomSheet() { } )) } + options.sortByDescending { it.usages } return options } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 63ba5e84..cbe83509 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -176,7 +176,7 @@ fun Note.getDisplayTime(): String { fun Note.getTagString(): String { val tags = getTags() - return tags.map { it -> '`' + it.title + '`' }.joinToString(separator = " ") + return tags.map { it -> "` ${it.title} `" }.joinToString(separator = " ") } fun Note.getTags(): Set { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index b1ed4265..156020f5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -24,7 +24,7 @@ import com.maubis.scarlet.base.note.reminders.sheet.ReminderBottomSheet import com.maubis.scarlet.base.note.selection.activity.KEY_SELECT_EXTRA_MODE import com.maubis.scarlet.base.note.selection.activity.KEY_SELECT_EXTRA_NOTE_ID import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity -import com.maubis.scarlet.base.note.tag.sheet.TagChooseOptionsBottomSheet +import com.maubis.scarlet.base.note.tag.sheet.TagChooserBottomSheet import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet @@ -92,10 +92,10 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { } } tagCardLayout.setOnClickListener { - TagChooseOptionsBottomSheet.openSheet( - activity, - note - ) { activity.notifyTagsChanged(note) } + com.maubis.scarlet.base.support.sheets.openSheet(activity, TagChooserBottomSheet().apply { + this.note = note + dismissListener = { activity.notifyTagsChanged(note) } + }) dismiss() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt deleted file mode 100644 index dcadc63b..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooseOptionsBottomSheet.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.maubis.scarlet.base.note.tag.sheet - -import android.app.Dialog -import android.content.DialogInterface -import android.view.View -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb -import com.maubis.scarlet.base.core.note.getTagUUIDs -import com.maubis.scarlet.base.core.tag.TagBuilder -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.save -import com.maubis.scarlet.base.note.tag.TagOptionsItem -import com.maubis.scarlet.base.note.toggleTag -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.ui.visibility - -class TagChooseOptionsBottomSheet : TagOptionItemBottomSheetBase() { - - var note: Note? = null - var dismissListener: () -> Unit = {} - - override fun setupViewWithDialog(dialog: Dialog) { - if (note === null) { - dismiss() - return - } - - val options = getOptions() - dialog.findViewById(R.id.tag_card_layout).visibility = visibility(options.isNotEmpty()) - setOptions(dialog, getOptions()) - } - - override fun onNewTagClick() { - val activity = context as ThemedActivity - CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag(), { tag, _ -> - note!!.toggleTag(tag) - note!!.save(activity) - reset(dialog) - }) - } - - override fun onDismiss(dialog: DialogInterface?) { - super.onDismiss(dialog) - dismissListener() - } - - private fun getOptions(): List { - val options = ArrayList() - val tags = note!!.getTagUUIDs() - for (tag in tagsDb.getAll()) { - options.add(TagOptionsItem( - tag = tag, - listener = View.OnClickListener { - note!!.toggleTag(tag) - note!!.save(themedContext()) - reset(dialog) - }, - selected = tags.contains(tag.uuid) - )) - } - return options - } - - companion object { - fun openSheet(activity: ThemedActivity, note: Note, dismissListener: () -> Unit) { - val sheet = TagChooseOptionsBottomSheet() - - sheet.note = note - sheet.dismissListener = dismissListener - sheet.show(activity.supportFragmentManager, sheet.tag) - } - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt new file mode 100644 index 00000000..3a7bdaf7 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt @@ -0,0 +1,88 @@ +package com.maubis.scarlet.base.note.tag.sheet + +import android.app.Dialog +import android.content.DialogInterface +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.widget.EmptyComponent +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.core.note.getTagUUIDs +import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.main.sheets.LithoTagOptionsItem +import com.maubis.scarlet.base.main.sheets.TagItemLayout +import com.maubis.scarlet.base.note.save +import com.maubis.scarlet.base.note.toggleTag +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.OptionItemLayout +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle + +class TagChooserBottomSheet : LithoBottomSheet() { + + var note: Note? = null + var dismissListener: () -> Unit = {} + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + if (note === null) { + dismiss() + return EmptyComponent.create(componentContext).build() + } + + val activity = context as MainActivity + val component = Column.create(componentContext) + .widthPercent(100f) + val tagsComponent = Column.create(componentContext) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child(getLithoBottomSheetTitle(componentContext) + .textRes(R.string.tag_sheet_choose_tag) + .marginDip(YogaEdge.BOTTOM, 12f)) + getTagOptions().forEach { + tagsComponent.child(TagItemLayout.create(componentContext).option(it)) + } + + val addTag = LithoOptionsItem( + title = R.string.tag_sheet_new_tag_button, + subtitle = 0, + icon = R.drawable.icon_add_note, + listener = { CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { _, _ -> reset(activity, dialog) } }) + tagsComponent.child(OptionItemLayout.create(componentContext) + .option(addTag) + .backgroundRes(R.drawable.accent_rounded_bg) + .marginDip(YogaEdge.TOP, 16f) + .onClick { addTag.listener() }) + + component.child(tagsComponent) + return component.build() + } + + private fun getTagOptions(): List { + val activity = context as MainActivity + val options = ArrayList() + val tags = note!!.getTagUUIDs() + for (tag in CoreConfig.tagsDb.getAll()) { + options.add(LithoTagOptionsItem( + tag = tag, + listener = { + note!!.toggleTag(tag) + note!!.save(activity) + reset(activity, dialog) + }, + isSelected = tags.contains(tag.uuid) + )) + } + options.sortByDescending { if (it.isSelected) 1 else 0 } + return options + } + + override fun onDismiss(dialog: DialogInterface?) { + super.onDismiss(dialog) + dismissListener() + } +} \ No newline at end of file diff --git a/base/src/main/res/layout/bottom_sheet_home_navigation.xml b/base/src/main/res/layout/bottom_sheet_home_navigation.xml deleted file mode 100644 index 4bf6fb3a..00000000 --- a/base/src/main/res/layout/bottom_sheet_home_navigation.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From afe9ab361dd7debb6d0de1c5c4455770d830a3c9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 23 May 2019 20:11:57 +0100 Subject: [PATCH 024/134] Migrating more tag options to Litho --- app/build.gradle | 4 +- .../sheet/SelectedNoteOptionsBottomSheet.kt | 24 ++--- .../SelectedTagChooseOptionsBottomSheet.kt | 71 --------------- .../sheet/SelectedTagChooserBottomSheet.kt | 91 +++++++++++++++++++ .../tag/sheet/TagOptionItemBottomSheetBase.kt | 72 --------------- build.gradle | 4 +- 6 files changed, 107 insertions(+), 159 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt delete mode 100644 base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt diff --git a/app/build.gradle b/app/build.gradle index b4eab6ad..a5753b8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 129 - versionName '6.13.0' + versionCode 130 + versionName '6.14.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt index a3eff2d1..d7bd15fd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt @@ -13,7 +13,7 @@ import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.folder.sheet.SelectedFolderChooseOptionsBottomSheet import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity -import com.maubis.scarlet.base.note.tag.sheet.SelectedTagChooseOptionsBottomSheet +import com.maubis.scarlet.base.note.tag.sheet.SelectedTagChooserBottomSheet import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase @@ -141,26 +141,26 @@ class SelectedNoteOptionsBottomSheet() : GridBottomSheetBase() { title = R.string.change_tags, subtitle = R.string.change_tags, icon = R.drawable.ic_action_tags, - listener = lockAwareFunctionRunner(activity, { - SelectedTagChooseOptionsBottomSheet.openSheet(activity, { - tag, selectTag -> - activity.runNoteFunction { - when (selectTag) { - true -> it.addTag(tag) - false -> it.removeTag(tag) + listener = lockAwareFunctionRunner(activity) { + com.maubis.scarlet.base.support.sheets.openSheet(activity, SelectedTagChooserBottomSheet().apply { + onActionListener = { tag, selectTag -> + activity.runNoteFunction { + when (selectTag) { + true -> it.addTag(tag) + false -> it.removeTag(tag) + } + it.save(activity) } - it.save(activity) } }) - }) + } )) options.add(OptionsItem( title = R.string.folder_option_change_notebook, subtitle = R.string.folder_option_change_notebook, icon = R.drawable.ic_folder, listener = lockAwareFunctionRunner(activity, { - SelectedFolderChooseOptionsBottomSheet.openSheet(activity, { - folder, selectFolder -> + SelectedFolderChooseOptionsBottomSheet.openSheet(activity, { folder, selectFolder -> activity.runNoteFunction { when (selectFolder) { true -> it.folder = folder.uuid diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt deleted file mode 100644 index 10e34eaf..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooseOptionsBottomSheet.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.maubis.scarlet.base.note.tag.sheet - -import android.app.Dialog -import android.view.View -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb -import com.maubis.scarlet.base.core.note.getTagUUIDs -import com.maubis.scarlet.base.core.tag.TagBuilder -import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity -import com.maubis.scarlet.base.note.tag.TagOptionsItem -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.ui.visibility - -class SelectedTagChooseOptionsBottomSheet : TagOptionItemBottomSheetBase() { - - var onActionListener: (Tag, Boolean) -> Unit = { _, _ -> } - - override fun setupViewWithDialog(dialog: Dialog) { - val options = getOptions() - dialog.findViewById(R.id.tag_card_layout).visibility = visibility(options.isNotEmpty()) - setOptions(dialog, getOptions()) - } - - override fun onNewTagClick() { - val activity = context as ThemedActivity - CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag(), { tag, _ -> - onActionListener(tag, true) - reset(dialog) - }) - } - - private fun getOptions(): List { - val activity = themedContext() as SelectNotesActivity - val options = ArrayList() - - val tags = HashSet() - tags.addAll(activity.getAllSelectedNotes().firstOrNull()?.getTagUUIDs() ?: emptySet()) - - activity.getAllSelectedNotes().forEach { - val uuids = it.getTagUUIDs().toMutableSet() - val uuidsToRemove = HashSet() - for (tag in tags) { - if (!uuids.contains(tag)) { - uuidsToRemove.add(tag) - } - } - tags.removeAll(uuidsToRemove) - } - for (tag in tagsDb.getAll()) { - options.add(TagOptionsItem( - tag = tag, - listener = View.OnClickListener { - onActionListener(tag, !tags.contains(tag.uuid)) - activity.refreshSelectedNotes() - reset(dialog) - }, - selected = tags.contains(tag.uuid) - )) - } - return options - } - - companion object { - fun openSheet(activity: ThemedActivity, onActionListener: (Tag, Boolean) -> Unit) { - val sheet = SelectedTagChooseOptionsBottomSheet() - sheet.onActionListener = onActionListener - sheet.show(activity.supportFragmentManager, sheet.tag) - } - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt new file mode 100644 index 00000000..91cac4bf --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt @@ -0,0 +1,91 @@ +package com.maubis.scarlet.base.note.tag.sheet + +import android.app.Dialog +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.core.note.getTagUUIDs +import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.main.sheets.LithoTagOptionsItem +import com.maubis.scarlet.base.main.sheets.TagItemLayout +import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.OptionItemLayout +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle + +class SelectedTagChooserBottomSheet : LithoBottomSheet() { + + var onActionListener: (Tag, Boolean) -> Unit = { _, _ -> } + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + val activity = context as SelectNotesActivity + val component = Column.create(componentContext) + .widthPercent(100f) + val tagsComponent = Column.create(componentContext) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child(getLithoBottomSheetTitle(componentContext) + .textRes(R.string.tag_sheet_choose_tag) + .marginDip(YogaEdge.BOTTOM, 12f)) + getTagOptions().forEach { + tagsComponent.child(TagItemLayout.create(componentContext).option(it)) + } + + val addTag = LithoOptionsItem( + title = R.string.tag_sheet_new_tag_button, + subtitle = 0, + icon = R.drawable.icon_add_note, + listener = { + CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { tag, _ -> + onActionListener(tag, true) + reset(activity, dialog) + } + }) + tagsComponent.child(OptionItemLayout.create(componentContext) + .option(addTag) + .backgroundRes(R.drawable.accent_rounded_bg) + .marginDip(YogaEdge.TOP, 16f) + .onClick { addTag.listener() }) + + component.child(tagsComponent) + return component.build() + } + + private fun getTagOptions(): List { + val activity = context as SelectNotesActivity + val options = ArrayList() + + val tags = HashSet() + tags.addAll(activity.getAllSelectedNotes().firstOrNull()?.getTagUUIDs() ?: emptySet()) + + activity.getAllSelectedNotes().forEach { + val uuids = it.getTagUUIDs().toMutableSet() + val uuidsToRemove = HashSet() + for (tag in tags) { + if (!uuids.contains(tag)) { + uuidsToRemove.add(tag) + } + } + tags.removeAll(uuidsToRemove) + } + for (tag in CoreConfig.tagsDb.getAll()) { + options.add(LithoTagOptionsItem( + tag = tag, + listener = { + onActionListener(tag, !tags.contains(tag.uuid)) + activity.refreshSelectedNotes() + reset(activity, dialog) + }, + isSelected = tags.contains(tag.uuid) + )) + } + options.sortByDescending { if (it.isSelected) 1 else 0 } + return options + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt deleted file mode 100644 index 46fb83ab..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagOptionItemBottomSheetBase.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.maubis.scarlet.base.note.tag.sheet - -import android.app.Dialog -import android.view.View -import android.view.View.GONE -import android.widget.LinearLayout -import com.github.bijoysingh.uibasics.views.UIActionView -import com.github.bijoysingh.uibasics.views.UITextView -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.note.tag.TagOptionsItem -import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment - -abstract class TagOptionItemBottomSheetBase : ThemedBottomSheetFragment() { - override fun setupView(dialog: Dialog?) { - super.setupView(dialog) - if (dialog == null) { - return - } - reset(dialog) - setAddTagOption(dialog) - makeBackgroundTransparent(dialog, R.id.root_layout) - } - - abstract fun setupViewWithDialog(dialog: Dialog) - - abstract fun onNewTagClick() - - override fun getBackgroundView(): Int { - return R.id.options_layout - } - - override fun getBackgroundCardViewIds(): Array = arrayOf(R.id.tag_card_layout) - - fun setAddTagOption(dialog: Dialog) { - val newTagButton = dialog.findViewById(R.id.new_tag_button); - newTagButton.setOnClickListener { onNewTagClick() } - newTagButton.icon.alpha = 0.6f - } - - fun reset(dialog: Dialog) { - val layout = dialog.findViewById(R.id.options_container) - layout.removeAllViews() - setupViewWithDialog(dialog) - } - - fun setOptions(dialog: Dialog, options: List) { - val layout = dialog.findViewById(R.id.options_container); - for (option in options) { - val contentView = View.inflate(context, R.layout.layout_option_sheet_item, null) as UIActionView - contentView.setTitle(option.tag.title) - contentView.setOnClickListener(option.listener) - contentView.subtitle.visibility = GONE - contentView.setImageResource(option.getIcon()) - - if (option.editable) { - contentView.setActionResource(option.getEditIcon()); - contentView.setActionTint(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)); - contentView.setActionClickListener(option.editListener) - } - - contentView.setTitleColor(getOptionsTitleColor(option.selected)) - contentView.setSubtitleColor(getOptionsSubtitleColor(option.selected)) - contentView.setImageTint(getOptionsTitleColor(option.selected)) - - layout.addView(contentView) - } - } - - override fun getLayout(): Int = R.layout.bottom_sheet_tag_options -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 87c3f617..0235b34d 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 129 - ext.appconfig_version = '6.13.0' + ext.appconfig_version_code = 130 + ext.appconfig_version = '6.14.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From e2796b8e73c8183733de5a2aec0494d60e0e6a02 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 24 May 2019 23:56:04 +0100 Subject: [PATCH 025/134] Fixing Cast Exception --- app/build.gradle | 4 ++-- .../scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt | 8 +++++--- build.gradle | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a5753b8b..bf6feb7e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 130 - versionName '6.14.0' + versionCode 131 + versionName '6.15.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt index 3a7bdaf7..4535a1d4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.note.tag.sheet import android.app.Dialog import android.content.DialogInterface +import android.support.v7.app.AppCompatActivity import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext @@ -21,6 +22,7 @@ import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.OptionItemLayout import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.ui.ThemedActivity class TagChooserBottomSheet : LithoBottomSheet() { @@ -30,10 +32,10 @@ class TagChooserBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { if (note === null) { dismiss() - return EmptyComponent.create(componentContext).build() + return Column.create(componentContext).widthPercent(100f).build() } - val activity = context as MainActivity + val activity = context as ThemedActivity val component = Column.create(componentContext) .widthPercent(100f) val tagsComponent = Column.create(componentContext) @@ -63,7 +65,7 @@ class TagChooserBottomSheet : LithoBottomSheet() { } private fun getTagOptions(): List { - val activity = context as MainActivity + val activity = context as AppCompatActivity val options = ArrayList() val tags = note!!.getTagUUIDs() for (tag in CoreConfig.tagsDb.getAll()) { diff --git a/build.gradle b/build.gradle index 0235b34d..6455df39 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 130 - ext.appconfig_version = '6.14.0' + ext.appconfig_version_code = 131 + ext.appconfig_version = '6.15.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From ee559cb6fcae907a93fdcd2a7937ed1bc60f8ec9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 31 May 2019 22:39:44 +0100 Subject: [PATCH 026/134] Updating the logic for Drive+Firebase situation --- .../com/maubis/scarlet/base/MainActivity.kt | 13 ++ .../base/config/auth/IAuthenticator.kt | 2 +- .../base/config/auth/NullAuthenticator.kt | 2 +- .../main/specs/MainActivityBottomBarSpec.kt | 41 ++++ .../sheet/SettingsOptionsBottomSheet.kt | 6 +- .../main/res/drawable/icon_sync_disabled.png | Bin 0 -> 990 bytes .../main/res/drawable/login_button_active.xml | 2 +- .../res/drawable/secondary_rounded_bg.xml | 15 ++ base/src/main/res/values/strings.xml | 12 + scarlet/src/main/AndroidManifest.xml | 2 +- .../quicknote/drive/GDriveActivitySpecs.kt | 13 +- .../quicknote/drive/GDriveAuthenticator.kt | 52 +++++ .../quicknote/drive/GDriveLoginActivity.kt | 8 + .../firebase/activity/DataPolicyActivity.kt | 2 +- .../activity/FirebaseActivitySpecs.kt | 117 ++++++++++ .../activity/FirebaseLoginActivity.kt | 147 ++++++++++++ .../firebase/activity/ForgetMeActivity.kt | 4 +- .../firebase/activity/LoginActivity.kt | 214 ------------------ .../firebase/support/FirebaseAuthenticator.kt | 92 ++++++++ .../firebase/support/ScarletAuthenticator.kt | 111 ++------- .../quicknote/scarlet/ScarletFolderActor.kt | 6 +- .../quicknote/scarlet/ScarletNoteActor.kt | 6 +- .../quicknote/scarlet/ScarletTagActor.kt | 6 +- .../src/main/res/layout/activity_login.xml | 142 ------------ 24 files changed, 553 insertions(+), 462 deletions(-) create mode 100644 base/src/main/res/drawable/icon_sync_disabled.png create mode 100644 base/src/main/res/drawable/secondary_rounded_bg.xml create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt delete mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt delete mode 100644 scarlet/src/main/res/layout/activity_login.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 6420dc3a..c53781aa 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -24,6 +24,7 @@ import com.maubis.scarlet.base.main.recycler.* import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteTrashSheet import com.maubis.scarlet.base.main.specs.MainActivityBottomBar +import com.maubis.scarlet.base.main.specs.MainActivityDisabledSync import com.maubis.scarlet.base.main.specs.MainActivityFolderBottomBar import com.maubis.scarlet.base.main.utils.MainSnackbar import com.maubis.scarlet.base.note.activity.INoteOptionSheetActivity @@ -87,6 +88,8 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { if (shouldShowWhatsNewSheet()) { openSheet(this, WhatsNewBottomSheet()) } + + notifyDisabledSync() } fun setListeners() { @@ -305,6 +308,15 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { .build())) } + fun notifyDisabledSync() { + val componentContext = ComponentContext(this) + lithoPreBottomToolbar.removeAllViews() + + lithoPreBottomToolbar.addView(LithoView.create(componentContext, + MainActivityDisabledSync.create(componentContext) + .build())) + } + fun unifiedSearch() { GlobalScope.launch(Dispatchers.Main) { val items = GlobalScope.async(Dispatchers.IO) { unifiedSearchSynchronous() } @@ -388,6 +400,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { config.clear() onHomeClick() notifyFolderChange() + notifyDisabledSync() } else -> super.onBackPressed() } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index c1881636..08941f9a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -8,7 +8,7 @@ interface IAuthenticator { fun isLoggedIn(): Boolean - fun userId(): String? + fun userId(context: Context): String? fun openLoginActivity(context: Context): Runnable? diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index 275d268d..e8c46f9f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -11,7 +11,7 @@ class NullAuthenticator : IAuthenticator { override fun setup(context: Context) {} - override fun userId(): String? = null + override fun userId(context: Context): String? = null override fun isLoggedIn(): Boolean = false diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index a219fa6b..a42e24e9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.main.specs import android.graphics.Color import android.text.Layout +import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.Row @@ -14,6 +15,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT_MEDIUM import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.main.sheets.HomeOptionsBottomSheet @@ -122,3 +124,42 @@ object MainActivityFolderBottomBarSpec { return bottomBarCard(context, row.build(), colorConfig).build() } } + +@LayoutSpec +object MainActivityDisabledSyncSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext): Component { + val colorConfig = ToolbarColorConfig( + toolbarBackgroundColor = context.getColor(R.color.material_blue_grey_800), + toolbarIconColor = context.getColor(R.color.light_secondary_text) + ) + val activity = context.androidContext as MainActivity + val row = Row.create(context) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 4f) + row.child(bottomBarRoundIcon(context, colorConfig) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.ic_info) + .onClick { + GlobalScope.launch { + + } + }) + row.child( + Column.create(context) + .flexGrow(1f) + .paddingDip(YogaEdge.ALL, 8f) + .child(Text.create(context) + .typeface(FONT_MONSERRAT_MEDIUM) + .textRes(R.string.firebase_no_sync_warning) + .textSizeRes(R.dimen.font_size_normal) + .textColor(colorConfig.toolbarIconColor)) + .child(Text.create(context) + .typeface(FONT_MONSERRAT) + .textRes(R.string.firebase_no_sync_warning_details) + .textSizeRes(R.dimen.font_size_small) + .textColor(colorConfig.toolbarIconColor))) + return bottomBarCard(context, row.build(), colorConfig).build() + } +} diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index 7e123af4..fd85b8ed 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -28,7 +28,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { val options = ArrayList() val loginClick = ApplicationBase.instance.authenticator().openLoginActivity(activity) - val firebaseUser = ApplicationBase.instance.authenticator().userId() + val firebaseLoggedIn = ApplicationBase.instance.authenticator().userId(activity) !== null val migrateToPro = getMigrateToProAppInformationItem(activity) options.add(LithoOptionsItem( @@ -50,7 +50,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { loginClick?.run() dismiss() }, - visible = loginClick !== null && firebaseUser === null + visible = loginClick !== null && !firebaseLoggedIn )) options.add(LithoOptionsItem( title = R.string.home_option_ui_experience, @@ -135,7 +135,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { ApplicationBase.instance.authenticator().logout() dismiss() }, - visible = firebaseUser !== null + visible = firebaseLoggedIn )) return options } diff --git a/base/src/main/res/drawable/icon_sync_disabled.png b/base/src/main/res/drawable/icon_sync_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..915d999e2a3b337e753028fe909c1a6e2c50e2d7 GIT binary patch literal 990 zcmV<410np0P)6_bU_U`l~sqekJYPF#mB}tMb zNs=T&GtTgVN0Rai(?9L)$OxP>}3i&{44G&XPzfTd)^R#cec#q0#; z;w(A>P~k3W-2!fx6>kvAOkfGxF~!AB0S{0kEB=C93Cm2C(8VR-0eRpp$VjNoOz6l& z4gn9y2YZp_smz2G=n6njbHD@g!z)!JtU?dqNn^mB@6Sp7lzgWo?;P-l*|PAdjczkE6jOf!N7B# zgbWD`1)##w3UC#%GvtLo!7G8Q7&U(S5t`^i=y&;GePTEBfgxc9)=&n{BeuRga1)`C zEJ}kVAsh2Dn9%j|z%PUz*I0!lJV59t*)b=h3GF5olfWj#7Lyg*5*k9IR9HeULI=r; z^9a4JDcloyiO?mo;fmH~#l@(nG zEg=<`z`}%j$%+c0Ii%teXe2aRRtzW9Bo&uH6QN^2BDW^#ijXIX;0vJLg&kh1M**)tqIi7kI+uC zVgX_sn-XqSv)G#0{IcUmLib6 - + diff --git a/base/src/main/res/drawable/secondary_rounded_bg.xml b/base/src/main/res/drawable/secondary_rounded_bg.xml new file mode 100644 index 00000000..68db64ec --- /dev/null +++ b/base/src/main/res/drawable/secondary_rounded_bg.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index e8ff71e3..087ece18 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -442,4 +442,16 @@ Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. Your photos are uploaded and synced across devices as well. + + + Restore Data from Firebase + Sign In with Google + Signing In… + + We used to store your information on Google Firebase. After logging in we will recover these to your device + Changes to notes will NOT be synced over to Google Firebase, and you need to login on to Google Drive + Once your notes are recovered, we will delete them from Google Firebase, and then log you out + + Your data is not being synced! + Data Sync based on legacy login is disabled, consider switching over to Google Drive based sync. diff --git a/scarlet/src/main/AndroidManifest.xml b/scarlet/src/main/AndroidManifest.xml index 7a1f52c9..027e6997 100644 --- a/scarlet/src/main/AndroidManifest.xml +++ b/scarlet/src/main/AndroidManifest.xml @@ -109,7 +109,7 @@ - + diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index e6db9dfc..61daf155 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -34,11 +34,15 @@ object GDriveRootViewSpec { .marginDip(YogaEdge.ALL, 8f) .childComponent(GDriveContentView.create(context))) .child(Text.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.VERTICAL, 8f) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.google_drive_page_login_firebase_button) .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(CoreConfig.FONT_MONSERRAT) + .clickHandler(GDriveRootView.onFirebaseClick(context))) .child(Row.create(context) .backgroundRes(R.drawable.accent_rounded_bg) .alignItems(YogaAlign.CENTER) @@ -65,6 +69,11 @@ object GDriveRootViewSpec { fun onGoogleClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { onClick() } + + @OnEvent(ClickEvent::class) + fun onFirebaseClick(context: ComponentContext, @Prop onFirebaseClick: () -> Unit) { + onFirebaseClick() + } } @LayoutSpec diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt new file mode 100644 index 00000000..9ab5f36d --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -0,0 +1,52 @@ +package com.bijoysingh.quicknote.drive + +import android.content.Context +import android.content.Intent +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.maubis.scarlet.base.config.auth.IAuthenticator +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference + +class GDriveAuthenticator() : IAuthenticator { + + var account: GoogleSignInAccount? = null + + override fun setup(context: Context) { + GlobalScope.launch { + val account = GoogleSignIn.getLastSignedInAccount(context) + if (account !== null) { + val helper = GDriveLoginActivity.getDriveHelper(context, account) + gDrive = GDriveRemoteDatabase(WeakReference(context)) + gDrive?.init(helper) + } + } + } + + override fun isLoggedIn(): Boolean { + return account !== null + } + + override fun userId(context: Context): String? { + if (account !== null) { + return account?.id + } + + account = GoogleSignIn.getLastSignedInAccount(context) + return account?.id + } + + override fun openLoginActivity(context: Context) = Runnable { + context.startActivity(Intent(context, GDriveLoginActivity::class.java)) + } + + override fun openForgetMeActivity(context: Context) = Runnable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun logout() { + + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 45617729..e048f959 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -7,6 +7,8 @@ import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData +import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity +import com.bijoysingh.quicknote.firebase.support.sGDriveLoggedIn import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView @@ -109,6 +111,10 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed signIn() } } + .onFirebaseClick { + context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) + finish() + } .loggingIn(state) .build() setContentView(LithoView.create(componentContext, component)) @@ -129,6 +135,8 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) gDrive?.init(mDriveServiceHelper!!) + sGDriveLoggedIn = true + GlobalScope.launch { val database = gDrive?.gDriveDatabase if (database === null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt index c5da563c..b1d8a354 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt @@ -48,7 +48,7 @@ class DataPolicyActivity : ThemedActivity() { if (acceptCheckBox.isChecked) { acceptThePolicy() if (startState == "" && !ApplicationBase.instance.authenticator().isLoggedIn()) { - IntentUtils.startActivity(this, LoginActivity::class.java) + IntentUtils.startActivity(this, FirebaseLoginActivity::class.java) } finish() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt new file mode 100644 index 00000000..3f8ff3d8 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt @@ -0,0 +1,117 @@ +package com.bijoysingh.quicknote.firebase.activity + +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.text.Layout +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.VerticalScroll +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.support.specs.color +import com.maubis.scarlet.base.support.ui.LithoCircleDrawable +import com.maubis.scarlet.base.support.ui.ThemeColorType + +@LayoutSpec +object FirebaseRootViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop loggingIn: Boolean): Component { + val buttonTitle = when { + loggingIn -> R.string.firebase_page_logging_in_button + else -> R.string.firebase_page_login_button + } + return Column.create(context) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(FirebaseContentView.create(context))) + .child(Row.create(context) + .backgroundRes(R.drawable.login_button_active) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.ic_google_icon) + .heightDip(36f) + ) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(FirebaseRootView.onGoogleClickEvent(context))) + .build() + } + + @OnEvent(ClickEvent::class) + fun onGoogleClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + +@LayoutSpec +object FirebaseContentViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext): Component { + return Column.create(context) + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.firebase_page_login_title) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.firebase_page_important_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(FirebaseIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.icon_sync_disabled) + .titleRes(R.string.firebase_page_not_sync_details)) + .child(FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_delete_permanently) + .titleRes(R.string.firebase_page_remove_details)) + .build() + } +} + +@LayoutSpec +object FirebaseIconViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop(resType = ResType.COLOR) bgColor: Int, + @Prop(resType = ResType.DRAWABLE) icon: Drawable, + @Prop(resType = ResType.STRING) title: String): Component { + return Column.create(context) + .paddingDip(YogaEdge.HORIZONTAL, 32f) + .paddingDip(YogaEdge.VERTICAL, 24f) + .child(Image.create(context) + .drawable(icon.color(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) + .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) + .paddingDip(YogaEdge.ALL, 12f) + .marginDip(YogaEdge.BOTTOM, 12f) + .heightDip(64f)) + .child(Text.create(context) + .text(title) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .textSizeRes(R.dimen.font_size_normal) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT)) + .build() + } +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt new file mode 100644 index 00000000..28fd2c3a --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -0,0 +1,147 @@ +package com.bijoysingh.quicknote.firebase.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import com.bijoysingh.quicknote.R +import com.bijoysingh.quicknote.firebase.initFirebaseDatabase +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.LithoView +import com.github.bijoysingh.starter.util.ToastHelper +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.Scope +import com.google.android.gms.tasks.OnCompleteListener +import com.google.android.gms.tasks.Task +import com.google.api.services.drive.DriveScopes +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.auth.GoogleAuthProvider +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN +import com.maubis.scarlet.base.support.ui.ThemedActivity +import java.util.concurrent.atomic.AtomicBoolean + +class FirebaseLoginActivity : ThemedActivity() { + + private val RC_SIGN_IN = 31245 + + lateinit var context: Context + lateinit var googleSignInClient: GoogleSignInClient + lateinit var firebaseAuth: FirebaseAuth + + lateinit var component: Component + lateinit var componentContext: ComponentContext + + var loggingIn = AtomicBoolean(false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + context = this + componentContext = ComponentContext(context) + setButton(false) + setupGoogleLogin() + firebaseAuth = FirebaseAuth.getInstance() + notifyThemeChange() + } + + private fun setButton(state: Boolean) { + loggingIn.set(state) + component = FirebaseRootView.create(componentContext) + .onClick { + if (!loggingIn.get()) { + setButton(true) + signIn() + } + } + .loggingIn(state) + .build() + setContentView(LithoView.create(componentContext, component)) + } + + private fun setupGoogleLogin() { + val gso = GoogleSignInOptions.Builder( + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() + + googleSignInClient = GoogleSignIn.getClient(this, gso); + } + + private fun signIn() { + val signInIntent = googleSignInClient.signInIntent + startActivityForResult(signInIntent, RC_SIGN_IN) + } + + override fun onBackPressed() { + if (!loggingIn.get()) { + super.onBackPressed() + } + } + + public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RC_SIGN_IN) { + val task = GoogleSignIn.getSignedInAccountFromIntent(data) + handleSignInResult(task) + } + } + + private fun handleSignInResult(task: Task) { + try { + val account = task.getResult(ApiException::class.java) + if (account !== null) { + firebaseAuthWithGoogle(account) + return + } + } catch (exception: Exception) { + Log.e("Firebase", exception.toString(), exception) + // Ignore this, handled by following content + } + onLoginFailure() + } + + private fun firebaseAuthWithGoogle(account: GoogleSignInAccount) { + val credential = GoogleAuthProvider.getCredential(account.idToken, null) + firebaseAuth.signInWithCredential(credential) + .addOnCompleteListener(this, object : OnCompleteListener { + override fun onComplete(task: Task) { + if (task.isSuccessful()) { + val user = firebaseAuth.currentUser + onLoginSuccess(user) + } else { + Log.e("Firebase", "Failed") + onLoginFailure() + } + } + }) + } + + private fun onLoginSuccess(user: FirebaseUser?) { + if (user === null || user.uid.isEmpty()) { + return + } + + ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) + setButton(false) + initFirebaseDatabase(context, user.uid) + finish() + } + + private fun onLoginFailure() { + ToastHelper.show(context, R.string.login_to_google_failed) + setButton(false) + } + + override fun notifyThemeChange() { + setSystemTheme() + } +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index cccc1e54..87a8f963 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -50,8 +50,8 @@ class ForgetMeActivity : ThemedActivity() { return@setOnClickListener } - val userId = ApplicationBase.instance.authenticator().userId() - if (userId === null) { + val isLoggedIn = ApplicationBase.instance.authenticator().isLoggedIn() + if (!isLoggedIn) { return@setOnClickListener } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt deleted file mode 100644 index 4afda03c..00000000 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/LoginActivity.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.bijoysingh.quicknote.firebase.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.widget.TextView -import com.bijoysingh.quicknote.R -import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy -import com.bijoysingh.quicknote.firebase.initFirebaseDatabase -import com.github.bijoysingh.starter.util.IntentUtils -import com.github.bijoysingh.starter.util.ToastHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount -import com.google.android.gms.auth.api.signin.GoogleSignInClient -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.tasks.OnCompleteListener -import com.google.android.gms.tasks.Task -import com.google.firebase.auth.AuthResult -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseUser -import com.google.firebase.auth.GoogleAuthProvider -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb -import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN -import com.maubis.scarlet.base.note.folder.saveToSync -import com.maubis.scarlet.base.note.saveToSync -import com.maubis.scarlet.base.note.tag.saveToSync -import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.utils.Flavor - - -class LoginActivity : ThemedActivity() { - - private val RC_SIGN_IN = 31244 - - lateinit var context: Context - lateinit var googleSignInClient: GoogleSignInClient - lateinit var firebaseAuth: FirebaseAuth - - lateinit var button: View - lateinit var installPro: View - lateinit var buttonTitle: TextView - var loggingIn = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) - context = this - setupSignInButton() - setupGoogleLogin() - firebaseAuth = FirebaseAuth.getInstance() - notifyThemeChange() - } - - override fun onResume() { - super.onResume() - if (!hasAcceptedThePolicy()) { - IntentUtils.startActivity(this, DataPolicyActivity::class.java) - finish() - } - } - - private fun setupSignInButton() { - button = findViewById(R.id.sign_in_button) - buttonTitle = button.findViewById(R.id.sign_in_button_title) - button.setOnClickListener { - if (loggingIn) { - // do nothing - } else { - setButton(true) - signIn() - } - } - - installPro = findViewById(R.id.install_pro) - installPro.setOnClickListener { - IntentUtils.openAppPlayStore(context, "com.bijoysingh.quicknote.pro") - finish() - } - - val proInformationVisibility = if (ApplicationBase.instance.appFlavor() == Flavor.PRO) View.GONE else View.VISIBLE - installPro.visibility = proInformationVisibility - val installProTitle = findViewById(R.id.install_pro_details_title) - installProTitle.visibility = proInformationVisibility - val installProDescription = findViewById(R.id.install_pro_details_description) - installProDescription.visibility = proInformationVisibility - val installProSeparator = findViewById(R.id.install_pro_separator) - installProSeparator.visibility = proInformationVisibility - } - - private fun setupGoogleLogin() { - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() - - googleSignInClient = GoogleSignIn.getClient(this, gso); - } - - private fun signIn() { - val signInIntent = googleSignInClient.signInIntent - startActivityForResult(signInIntent, RC_SIGN_IN) - } - - override fun onBackPressed() { - if (!loggingIn) { - super.onBackPressed() - } - } - - public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == RC_SIGN_IN) { - val task = GoogleSignIn.getSignedInAccountFromIntent(data) - handleSignInResult(task) - } - } - - private fun handleSignInResult(task: Task) { - try { - val account = task.getResult(ApiException::class.java) - if (account !== null) { - firebaseAuthWithGoogle(account) - return - } - } catch (exception: Exception) { - // Ignore this, handled by following content - } - - ToastHelper.show(context, R.string.login_to_google_failed) - setButton(false) - } - - private fun setButton(state: Boolean) { - loggingIn = state - if (loggingIn) { - button.setBackgroundResource(R.drawable.login_button_disabled) - buttonTitle.setText(R.string.logging_into_app) - } else { - button.setBackgroundResource(R.drawable.login_button_active) - buttonTitle.setText(R.string.login_with_google) - } - } - - private fun firebaseAuthWithGoogle(account: GoogleSignInAccount) { - val credential = GoogleAuthProvider.getCredential(account.idToken, null) - firebaseAuth.signInWithCredential(credential) - .addOnCompleteListener(this, object : OnCompleteListener { - override fun onComplete(task: Task) { - if (task.isSuccessful()) { - val user = firebaseAuth.currentUser - onLoginSuccess(user) - } else { - ToastHelper.show(context, R.string.login_to_google_failed) - setButton(false) - } - } - }) - } - - private fun onLoginSuccess(user: FirebaseUser?) { - ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) - transitionNotesToServer(user) - buttonTitle.setText(R.string.logged_into_app) - } - - private fun transitionNotesToServer(user: FirebaseUser?) { - if (user === null || user.uid.isEmpty()) { - return - } - - initFirebaseDatabase(context, user.uid) - for (note in notesDb.getAll()) { - if (note.disableBackup) { - continue - } - note.saveToSync(context) - } - for (tag in tagsDb.getAll()) { - tag.saveToSync() - } - for (folder in foldersDb.getAll()) { - folder.saveToSync() - } - finish() - } - - override fun notifyThemeChange() { - setSystemTheme(); - - val containerLayout = findViewById(R.id.container_layout); - containerLayout.setBackgroundColor(getThemeColor()); - - val optionsTitle = findViewById(R.id.sign_in_title) - optionsTitle.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - - val textColor = ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT) - val installProDescription = findViewById(R.id.install_pro_details_description) - val cloudSyncDescription = findViewById(R.id.cloud_sync_details_description) - installProDescription.setTextColor(textColor) - cloudSyncDescription.setTextColor(textColor) - - val installProTitle = findViewById(R.id.install_pro_details_title) - val cloudSyncTitle = findViewById(R.id.cloud_sync_details_title) - val titleTextColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER) - installProTitle.setTextColor(titleTextColor) - cloudSyncTitle.setTextColor(titleTextColor) - } -} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt new file mode 100644 index 00000000..c3c53a66 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt @@ -0,0 +1,92 @@ +package com.bijoysingh.quicknote.firebase.support + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import com.bijoysingh.quicknote.Scarlet.Companion.firebase +import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity +import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity +import com.bijoysingh.quicknote.firebase.initFirebaseDatabase +import com.github.bijoysingh.starter.async.SimpleThreadExecutor +import com.github.bijoysingh.starter.util.ToastHelper +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseNetworkException +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.database.FirebaseDatabase +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.auth.IAuthenticator +import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class FirebaseAuthenticator() : IAuthenticator { + + var userId: String? = null + + override fun userId(context: Context): String? { + val user = FirebaseAuth.getInstance().currentUser + return user?.uid + } + + override fun setup(context: Context) { + GlobalScope.launch { + FirebaseApp.initializeApp(context) + try { + userId = userId(context) + if (userId === null) { + return@launch + } + + FirebaseDatabase.getInstance() + initFirebaseDatabase(context, userId!!) + reloadUser(context) + } catch (exception: Exception) { + // Don't need to do anything + } + } + } + + override fun isLoggedIn(): Boolean { + return userId !== null + } + + override fun logout() { + userId = null + FirebaseAuth.getInstance().signOut() + firebase?.logout() + } + + override fun openLoginActivity(context: Context) = Runnable { + context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) + } + + override fun openForgetMeActivity(context: Context) = Runnable { + context.startActivity(Intent(context, ForgetMeActivity::class.java)) + } + + private fun reloadUser(context: Context) { + SimpleThreadExecutor.execute { + try { + FirebaseAuth.getInstance().currentUser?.reload()?.addOnCompleteListener { + if (it.isSuccessful) { + return@addOnCompleteListener + } + val exception = it.exception + if (exception !== null && exception is FirebaseNetworkException) { + return@addOnCompleteListener + } + + logout() + ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) + val handler = Handler(Looper.getMainLooper()) + handler.post { + ToastHelper.show(context, "You have been signed out of the app") + } + } + } catch (e: Exception) { + // In case somehow it fails + } + } + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt index 1629762a..83f95f8b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt @@ -1,107 +1,42 @@ package com.bijoysingh.quicknote.firebase.support import android.content.Context -import android.content.Intent -import android.os.Handler -import android.os.Looper -import com.bijoysingh.quicknote.Scarlet.Companion.firebase -import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.drive.GDriveLoginActivity -import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase -import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity -import com.bijoysingh.quicknote.firebase.activity.LoginActivity -import com.bijoysingh.quicknote.firebase.initFirebaseDatabase -import com.github.bijoysingh.starter.async.SimpleThreadExecutor -import com.github.bijoysingh.starter.util.ToastHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseNetworkException -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.database.FirebaseDatabase -import com.maubis.scarlet.base.config.ApplicationBase +import com.bijoysingh.quicknote.Scarlet +import com.bijoysingh.quicknote.drive.GDriveAuthenticator import com.maubis.scarlet.base.config.auth.IAuthenticator -import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.lang.ref.WeakReference -class ScarletAuthenticator() : IAuthenticator { - override fun userId(): String? { - val user = FirebaseAuth.getInstance().currentUser - return user?.uid - } +const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" +var sGDriveLoggedIn: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit - override fun setup(context: Context) { - /* - // TODO: Uncomment this when GDrive is ready - GlobalScope.launch { - val account = GoogleSignIn.getLastSignedInAccount(context) - if (account !== null) { - val helper= GDriveLoginActivity.getDriveHelper(context, account) - gDrive = GDriveRemoteDatabase(WeakReference(context)) - gDrive?.init(helper) - } - } - */ +const val KEY_FIREBASE_KILLED = "firebase_killed" +var sFirebaseKilled: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_FIREBASE_KILLED, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit - GlobalScope.launch { - FirebaseApp.initializeApp(context) - try { - val userId = userId() - if (userId === null) { - return@launch - } +class ScarletAuthenticator() : IAuthenticator { - FirebaseDatabase.getInstance() - initFirebaseDatabase(context, userId) - reloadUser(context) - } catch (exception: Exception) { - // Don't need to do anything - } - } - } + val firebase = FirebaseAuthenticator() + val gdrive = GDriveAuthenticator() - override fun isLoggedIn(): Boolean { - return userId() !== null + override fun userId(context: Context): String? { + return firebase.userId(context) } - override fun logout() { - FirebaseAuth.getInstance().signOut() - firebase?.logout() + override fun setup(context: Context) { + firebase.setup(context) } - override fun openLoginActivity(context: Context) = Runnable { - // TODO: Uncomment this when GDrive is ready - // context.startActivity(Intent(context, GDriveLoginActivity::class.java)) - context.startActivity(Intent(context, LoginActivity::class.java)) + override fun isLoggedIn(): Boolean { + return firebase.isLoggedIn() } - override fun openForgetMeActivity(context: Context) = Runnable { - context.startActivity(Intent(context, ForgetMeActivity::class.java)) + override fun logout() { + firebase.logout() } - fun reloadUser(context: Context) { - SimpleThreadExecutor.execute { - try { - FirebaseAuth.getInstance().currentUser?.reload()?.addOnCompleteListener { - if (it.isSuccessful) { - return@addOnCompleteListener - } - val exception = it.exception - if (exception !== null && exception is FirebaseNetworkException) { - return@addOnCompleteListener - } + override fun openLoginActivity(context: Context) = gdrive.openLoginActivity(context) - logout() - ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) - val handler = Handler(Looper.getMainLooper()) - handler.post { - ToastHelper.show(context, "You have been signed out of the app") - } - } - } catch (e: Exception) { - // In case somehow it fails - } - } - } + override fun openForgetMeActivity(context: Context) = firebase.openForgetMeActivity(context) } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 78264425..4ea678d7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -10,13 +10,15 @@ class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { override fun onlineSave() { super.onlineSave() - firebase?.insert(folder.getFirebaseFolder()) + // TODO: Remove this completely, Not doing this anymore. + // firebase?.insert(folder.getFirebaseFolder()) gDrive?.notifyInsert(folder.getFirebaseFolder()) } override fun delete() { super.delete() - firebase?.remove(folder.getFirebaseFolder()) + // TODO: Remove this completely, Not doing this anymore. + // firebase?.remove(folder.getFirebaseFolder()) gDrive?.notifyRemove(folder.getFirebaseFolder()) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index d60d5871..334330a6 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -11,13 +11,15 @@ class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { override fun onlineSave(context: Context) { super.onlineSave(context) - firebase?.insert(note.getFirebaseNote()) + // TODO: Remove this completely, Not doing this anymore. + // firebase?.insert(note.getFirebaseNote()) gDrive?.notifyInsert(note.getFirebaseNote()) } override fun onlineDelete(context: Context) { super.onlineDelete(context) - firebase?.remove(note.getFirebaseNote()) + // TODO: Remove this completely, Not doing this anymore. + // firebase?.remove(note.getFirebaseNote()) gDrive?.notifyRemove(note.getFirebaseNote()) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 40a53301..5c67876e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -10,13 +10,15 @@ class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { override fun onlineSave() { super.onlineSave() - firebase?.insert(tag.getFirebaseTag()) + // TODO: Remove this completely, Not doing this anymore. + // firebase?.insert(tag.getFirebaseTag()) gDrive?.notifyInsert(tag.getFirebaseTag()) } override fun delete() { super.delete() - firebase?.remove(tag.getFirebaseTag()) + // TODO: Remove this completely, Not doing this anymore. + // firebase?.remove(tag.getFirebaseTag()) gDrive?.notifyRemove(tag.getFirebaseTag()) } } \ No newline at end of file diff --git a/scarlet/src/main/res/layout/activity_login.xml b/scarlet/src/main/res/layout/activity_login.xml deleted file mode 100644 index 099a74e2..00000000 --- a/scarlet/src/main/res/layout/activity_login.xml +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 9d219577d57435111896861d36934676861a32f6 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 1 Jun 2019 00:50:22 +0100 Subject: [PATCH 027/134] Fixing Google Drive Login --- .../com/maubis/scarlet/base/MainActivity.kt | 14 ++- .../scarlet/base/config/ApplicationBase.kt | 5 +- .../base/config/auth/IAuthenticator.kt | 6 +- .../base/config/auth/NullAuthenticator.kt | 6 +- .../main/recycler/InformationRecyclerItem.kt | 4 +- .../main/specs/MainActivityBottomBarSpec.kt | 12 ++- .../sheet/DeleteAndMoreOptionsBottomSheet.kt | 2 +- .../res/drawable/login_button_disabled.xml | 6 +- base/src/main/res/values/strings.xml | 7 ++ scarlet/build.gradle | 2 +- scarlet/src/main/AndroidManifest.xml | 1 + .../java/com/bijoysingh/quicknote/Scarlet.kt | 3 + .../quicknote/drive/GDriveAuthenticator.kt | 44 ++++----- .../quicknote/drive/GDriveLoginActivity.kt | 5 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 1 - .../quicknote/drive/GDriveServiceHelper.kt | 30 +++--- .../firebase/activity/DataPolicyActivity.kt | 8 +- .../activity/FirebaseLoginActivity.kt | 8 +- .../activity/FirebaseRemovalActivity.kt | 78 ++++++++++++++++ .../activity/FirebaseRemovalActivitySpecs.kt | 91 +++++++++++++++++++ .../firebase/activity/ForgetMeActivity.kt | 2 +- .../firebase/support/FirebaseAuthenticator.kt | 31 +++---- .../firebase/support/FirebaseTagReference.kt | 3 +- .../firebase/support/ScarletAuthenticator.kt | 42 --------- .../quicknote/scarlet/ScarletAuthenticator.kt | 73 +++++++++++++++ .../quicknote/scarlet/ScarletConfig.kt | 1 - 26 files changed, 360 insertions(+), 125 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt delete mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index c53781aa..59c63411 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -14,6 +14,7 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag @@ -88,8 +89,6 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { if (shouldShowWhatsNewSheet()) { openSheet(this, WhatsNewBottomSheet()) } - - notifyDisabledSync() } fun setListeners() { @@ -240,7 +239,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { private fun addInformationItem(index: Int) { val informationItem = when { shouldShowMigrateToProAppInformationItem(this) -> getMigrateToProAppInformationItem(this) - shouldShowSignInformationItem() -> getSignInInformationItem(this) + shouldShowSignInformationItem(this) -> getSignInInformationItem(this) shouldShowAppUpdateInformationItem() -> getAppUpdateInformationItem(this) shouldShowReviewInformationItem() -> getReviewInformationItem(this) shouldShowInstallProInformationItem() -> getInstallProInformationItem(this) @@ -311,9 +310,15 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { fun notifyDisabledSync() { val componentContext = ComponentContext(this) lithoPreBottomToolbar.removeAllViews() + if (!instance.authenticator().isLegacyLoggedIn()) { + return + } lithoPreBottomToolbar.addView(LithoView.create(componentContext, MainActivityDisabledSync.create(componentContext) + .onClick { + instance.authenticator().openTransferDataActivity(componentContext.androidContext)?.run() + } .build())) } @@ -341,10 +346,11 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { GlobalScope.launch { ApplicationBase.instance.resyncDrive(false) { GlobalScope.launch(Dispatchers.Main) { - topSyncingLayout.visibility = View.GONE + topSyncingLayout.visibility = GONE } } } + notifyDisabledSync() } fun resetAndSetupData() { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index 73988546..5cc1f82d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -6,12 +6,15 @@ import com.facebook.soloader.SoLoader import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator +import java.lang.Exception abstract class ApplicationBase : Application() { override fun onCreate() { super.onCreate() SoLoader.init(this, false) - JobManager.create(this).addJobCreator(ReminderJobCreator()) + try { + JobManager.create(this).addJobCreator(ReminderJobCreator()) + } catch (exception: Exception) {} noteImagesFolder = NoteImage(this) } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index 08941f9a..7c1dc72b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -6,7 +6,9 @@ interface IAuthenticator { fun setup(context: Context) - fun isLoggedIn(): Boolean + fun isLoggedIn(context: Context): Boolean + + fun isLegacyLoggedIn(): Boolean fun userId(context: Context): String? @@ -14,5 +16,7 @@ interface IAuthenticator { fun openForgetMeActivity(context: Context): Runnable? + fun openTransferDataActivity(context: Context): Runnable? + fun logout() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index e8c46f9f..d6dacce8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -7,12 +7,16 @@ class NullAuthenticator : IAuthenticator { override fun openForgetMeActivity(context: Context): Runnable? = null + override fun openTransferDataActivity(context: Context): Runnable? = null + override fun logout() {} override fun setup(context: Context) {} override fun userId(context: Context): String? = null - override fun isLoggedIn(): Boolean = false + override fun isLoggedIn(context: Context): Boolean = false + + override fun isLegacyLoggedIn(): Boolean = false } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt index 6e1fb426..5f87d24c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt @@ -115,8 +115,8 @@ fun getInstallProInformationItem(context: Context): InformationRecyclerItem { } } -fun shouldShowSignInformationItem(): Boolean { - if (ApplicationBase.instance.authenticator().isLoggedIn() +fun shouldShowSignInformationItem(context: Context): Boolean { + if (ApplicationBase.instance.authenticator().isLoggedIn(context) || ApplicationBase.instance.appFlavor() == Flavor.NONE) { return false } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index a42e24e9..e0ce7b2c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -2,12 +2,10 @@ package com.maubis.scarlet.base.main.specs import android.graphics.Color import android.text.Layout -import com.facebook.litho.Column -import com.facebook.litho.Component -import com.facebook.litho.ComponentContext -import com.facebook.litho.Row +import com.facebook.litho.* import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent import com.facebook.litho.annotations.Prop import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign @@ -160,6 +158,12 @@ object MainActivityDisabledSyncSpec { .textRes(R.string.firebase_no_sync_warning_details) .textSizeRes(R.dimen.font_size_small) .textColor(colorConfig.toolbarIconColor))) + row.clickHandler(MainActivityDisabledSync.onClickEvent(context)) return bottomBarCard(context, row.build(), colorConfig).build() } + + @OnEvent(ClickEvent::class) + fun onClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt index 98503353..06fdc0d9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt @@ -95,7 +95,7 @@ class DeleteAndMoreOptionsBottomSheet : LithoOptionBottomSheet() { forgetMeClick?.run() dismiss() }, - visible = forgetMeClick !== null && ApplicationBase.instance.authenticator().isLoggedIn() + visible = forgetMeClick !== null && ApplicationBase.instance.authenticator().isLegacyLoggedIn() )) return options } diff --git a/base/src/main/res/drawable/login_button_disabled.xml b/base/src/main/res/drawable/login_button_disabled.xml index 8c904c9a..553e2314 100644 --- a/base/src/main/res/drawable/login_button_disabled.xml +++ b/base/src/main/res/drawable/login_button_disabled.xml @@ -5,11 +5,11 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > - - + + + android:color="@color/material_grey_800"/> diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 087ece18..d9705e46 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -454,4 +454,11 @@ Your data is not being synced! Data Sync based on legacy login is disabled, consider switching over to Google Drive based sync. + + Transfer Data from Firebase to Drive + Clear Data and Sign Out + Clearing and Signing Out + We used to store your information on Google Firebase. We are moving over to storing your notes in your Google Drive. + We will first delete your notes from Firebase and log you out. + You can then log into Google Drive, and we will upload your data along with images. diff --git a/scarlet/build.gradle b/scarlet/build.gradle index f39c9548..809ccfa3 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation ('com.google.http-client:google-http-client-gson:1.26.0') { - exclude group: 'org.apache.httpcomponents' + // exclude group: 'org.apache.httpcomponents' } implementation ('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' diff --git a/scarlet/src/main/AndroidManifest.xml b/scarlet/src/main/AndroidManifest.xml index 027e6997..306f2b4b 100644 --- a/scarlet/src/main/AndroidManifest.xml +++ b/scarlet/src/main/AndroidManifest.xml @@ -110,6 +110,7 @@ + diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index 34403303..592c5ff1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -11,7 +11,10 @@ class Scarlet : ApplicationBase() { override fun onCreate() { super.onCreate() + + gDriveConfig = Store.get(this, "gdrive_config") ApplicationBase.instance = ScarletConfig(this) + ApplicationBase.instance.themeController().setup(this) ApplicationBase.instance.authenticator().setup(this) ApplicationBase.instance.remoteConfigFetcher().setup(this) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt index 9ab5f36d..62b47657 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -1,52 +1,54 @@ package com.bijoysingh.quicknote.drive import android.content.Context -import android.content.Intent import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount -import com.maubis.scarlet.base.config.auth.IAuthenticator import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicBoolean -class GDriveAuthenticator() : IAuthenticator { +class GDriveAuthenticator { + var hasAccountSetup: AtomicBoolean = AtomicBoolean(false) var account: GoogleSignInAccount? = null - override fun setup(context: Context) { + fun setup(context: Context) { GlobalScope.launch { - val account = GoogleSignIn.getLastSignedInAccount(context) - if (account !== null) { - val helper = GDriveLoginActivity.getDriveHelper(context, account) + account = GoogleSignIn.getLastSignedInAccount(context) + hasAccountSetup.set(true) + + val signInAccount = account + if (signInAccount !== null) { + val helper = GDriveLoginActivity.getDriveHelper(context, signInAccount) gDrive = GDriveRemoteDatabase(WeakReference(context)) gDrive?.init(helper) } } } - override fun isLoggedIn(): Boolean { - return account !== null - } - - override fun userId(context: Context): String? { - if (account !== null) { - return account?.id + fun isLoggedIn(context: Context): Boolean { + if (hasAccountSetup.get()) { + return account !== null } account = GoogleSignIn.getLastSignedInAccount(context) - return account?.id - } + hasAccountSetup.set(true) - override fun openLoginActivity(context: Context) = Runnable { - context.startActivity(Intent(context, GDriveLoginActivity::class.java)) + return account !== null } - override fun openForgetMeActivity(context: Context) = Runnable { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + fun userId(context: Context): String? { + if (!hasAccountSetup.get()) { + account = GoogleSignIn.getLastSignedInAccount(context) + hasAccountSetup.set(true) + } + + return account?.id } - override fun logout() { + fun logout() { } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index e048f959..57bfe5bd 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -8,7 +8,7 @@ import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity -import com.bijoysingh.quicknote.firebase.support.sGDriveLoggedIn +import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView @@ -93,6 +93,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed try { val account = task.getResult(ApiException::class.java) if (account !== null) { + sGDriveLoggedIn = true onLoginComplete(account) return } @@ -135,8 +136,6 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) gDrive?.init(mDriveServiceHelper!!) - sGDriveLoggedIn = true - GlobalScope.launch { val database = gDrive?.gDriveDatabase if (database === null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 27b1ebf4..5e10ae5e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -85,7 +85,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { isValidController = true driveHelper = helper gDriveDatabase = genGDriveUploadDatabase(context) - gDriveConfig = Store.get(context, "gdrive_config") notesSync = GDriveRemoteFolder(GDriveDataType.NOTE, gDriveDatabase!!, helper) { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index ce6946ac..b69d5dc3 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -31,7 +31,7 @@ const val INVALID_FILE_ID = "__invalid__" var lastCheckpointTime: AtomicLong = AtomicLong(0L) var numQueriesSinceLastCheckpoint: AtomicLong = AtomicLong(0L) -class ErrorCallable(val callable: Callable) : Callable { +class ErrorCallable(val action: String, val callable: Callable) : Callable { override fun call(): T? { val lastCheckpoint = lastCheckpointTime.get() if (lastCheckpoint == 0L) { @@ -40,11 +40,11 @@ class ErrorCallable(val callable: Callable) : Callable { val currentCount = numQueriesSinceLastCheckpoint.get() * 1.0 val deltaTimeS = (System.currentTimeMillis() - lastCheckpointTime.get()) / 1000.0 - log("GDrive", "Request being called: currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=${currentCount/deltaTimeS}") + log("GDrive", "Request being called: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=${currentCount / deltaTimeS}") if (currentCount >= 10 && deltaTimeS > 0) { when { (currentCount / deltaTimeS) > 0.9 -> { - log("GDrive", "Rate limiting measures taken: currentCount=$currentCount, deltaTimeS=$deltaTimeS") + log("GDrive", "Rate limiting measures taken: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS") SystemClock.sleep(500L) return call() } @@ -80,14 +80,14 @@ fun getTrueCurrentTime(): Long { class GDriveServiceHelper(private val mDriveService: Drive) { private val mExecutor = Executors.newFixedThreadPool(8) - fun execute(callable: Callable): Task { - return Tasks.call(mExecutor, ErrorCallable(callable)) + fun execute(action: String = "", callable: Callable): Task { + return Tasks.call(mExecutor, ErrorCallable(action, callable)) } fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { log("GDrive", "createFileWithData($folderId, $name)") val contentToSave = if (content.isEmpty()) updateTime.toString() else content - return execute(Callable { + return execute("createFileWithData", Callable { val metadata = File() .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) @@ -100,7 +100,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { log("GDrive", "createFileWithData($folderId, $name, ${file.absolutePath})") - return execute(Callable { + return execute("createFileWithData", Callable { val metadata = File() .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) @@ -113,7 +113,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun createFolder(parentUid: String, folderName: String): Task { log("GDrive", "createFolder($parentUid, $folderName)") - return execute(Callable { + return execute("createFolder", Callable { val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) .setModifiedTime(DateTime(getTrueCurrentTime())) @@ -127,7 +127,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun readFile(fileId: String): Task { log("GDrive", "readFile($fileId)") - return execute(Callable { + return execute("readFile", Callable { mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> BufferedReader(InputStreamReader(`is`)).use { reader -> val stringBuilder = StringBuilder() @@ -145,7 +145,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun readFile(fileId: String, destinationFile: java.io.File): Task { log("GDrive", "readFile($fileId, ${destinationFile.absolutePath})") - return execute(Callable { + return execute("readFile", Callable { destinationFile.parentFile.mkdirs() val fileStream = FileOutputStream(destinationFile) mDriveService.files().get(fileId).executeMediaAndDownloadTo(fileStream) @@ -155,7 +155,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { log("GDrive", "saveFile($fileId, $name)") - return execute(Callable { + return execute("saveFile", Callable { val metadata = File().setModifiedTime(DateTime(updateTime)).setName(name) val contentStream = ByteArrayContent.fromString("text/plain", content) mDriveService.files().update(fileId, metadata, contentStream).execute() @@ -164,7 +164,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { log("GDrive", "getFilesInFolder($parentUid, $mimeType)") - return execute(Callable { + return execute("getFilesInFolder", Callable { mDriveService.files().list() .setSpaces("drive") .setPageSize(1000) @@ -180,7 +180,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { parentUid.isEmpty() -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name'" else -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name' and '$parentUid' in parents" } - return execute(Callable { + return execute("getFolderQuery", Callable { mDriveService.files().list() .setSpaces("drive") .setQ(query) @@ -194,7 +194,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { names.subList(1, names.lastIndex + 1).forEach { nameQueryBuilder += " or name = '$it'" } - return execute(Callable { + return execute("getSubRootFolders", Callable { mDriveService.files().list() .setSpaces("drive") .setQ("mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and ($nameQueryBuilder) and '$parentUid' in parents") @@ -204,7 +204,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun removeFileOrFolder(fileUid: String): Task { log("GDrive", "removeFileOrFolder($fileUid)") - return execute(Callable { + return execute("removeFileOrFolder", Callable { mDriveService.files().delete(fileUid) null }) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt index b1d8a354..4f56e47a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt @@ -47,7 +47,7 @@ class DataPolicyActivity : ThemedActivity() { doneBtn.setOnClickListener { if (acceptCheckBox.isChecked) { acceptThePolicy() - if (startState == "" && !ApplicationBase.instance.authenticator().isLoggedIn()) { + if (startState == "" && !ApplicationBase.instance.authenticator().isLegacyLoggedIn()) { IntentUtils.startActivity(this, FirebaseLoginActivity::class.java) } @@ -61,7 +61,7 @@ class DataPolicyActivity : ThemedActivity() { refuseBtn.setOnClickListener { IntentUtils.startActivity(this, ForgetMeActivity::class.java) } - refuseBtn.visibility = visibility(ApplicationBase.instance.authenticator().isLoggedIn()) + refuseBtn.visibility = visibility(ApplicationBase.instance.authenticator().isLegacyLoggedIn()) privacyPolicy.setOnClickListener { startActivity(Intent( @@ -76,7 +76,7 @@ class DataPolicyActivity : ThemedActivity() { override fun onResume() { super.onResume() - if (startState == KEY_DATA_POLICY_REQUEST_LOGGED_IN && !ApplicationBase.instance.authenticator().isLoggedIn()) { + if (startState == KEY_DATA_POLICY_REQUEST_LOGGED_IN && !ApplicationBase.instance.authenticator().isLegacyLoggedIn()) { finish() } } @@ -91,7 +91,7 @@ class DataPolicyActivity : ThemedActivity() { fun acceptThePolicy() = ApplicationBase.instance.store().put(DATA_POLICY_ACCEPTED, DATA_POLICY_VERSION) fun openIfNeeded(activity: AppCompatActivity) { - if (!hasAcceptedThePolicy() && ApplicationBase.instance.authenticator().isLoggedIn()) { + if (!hasAcceptedThePolicy() && ApplicationBase.instance.authenticator().isLegacyLoggedIn()) { val intent = Intent(activity, DataPolicyActivity::class.java) intent.putExtra(KEY_DATA_POLICY_REQUEST, KEY_DATA_POLICY_REQUEST_LOGGED_IN) activity.startActivity(intent) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt index 28fd2c3a..23605f96 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -5,10 +5,12 @@ import android.content.Intent import android.os.Bundle import android.util.Log import com.bijoysingh.quicknote.R +import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy import com.bijoysingh.quicknote.firebase.initFirebaseDatabase import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView +import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount @@ -55,6 +57,10 @@ class FirebaseLoginActivity : ThemedActivity() { loggingIn.set(state) component = FirebaseRootView.create(componentContext) .onClick { + if (!hasAcceptedThePolicy()) { + IntentUtils.startActivity(this, DataPolicyActivity::class.java) + return@onClick + } if (!loggingIn.get()) { setButton(true) signIn() @@ -73,7 +79,7 @@ class FirebaseLoginActivity : ThemedActivity() { .requestEmail() .build() - googleSignInClient = GoogleSignIn.getClient(this, gso); + googleSignInClient = GoogleSignIn.getClient(this, gso) } private fun signIn() { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt new file mode 100644 index 00000000..ab30fd12 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt @@ -0,0 +1,78 @@ +package com.bijoysingh.quicknote.firebase.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import com.bijoysingh.quicknote.R +import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy +import com.bijoysingh.quicknote.firebase.initFirebaseDatabase +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.LithoView +import com.github.bijoysingh.starter.util.IntentUtils +import com.github.bijoysingh.starter.util.ToastHelper +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.Scope +import com.google.android.gms.tasks.OnCompleteListener +import com.google.android.gms.tasks.Task +import com.google.api.services.drive.DriveScopes +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.auth.GoogleAuthProvider +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN +import com.maubis.scarlet.base.support.ui.ThemedActivity +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean + +class FirebaseRemovalActivity : ThemedActivity() { + + lateinit var context: Context + lateinit var component: Component + lateinit var componentContext: ComponentContext + + var loggingIn = AtomicBoolean(false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + context = this + componentContext = ComponentContext(context) + setButton(false) + notifyThemeChange() + } + + private fun setButton(state: Boolean) { + loggingIn.set(state) + component = FirebaseRemovalRootView.create(componentContext) + .onClick { + // TODO: Delete remote data... + setButton(true) + GlobalScope.launch { + instance.authenticator().logout() + setButton(false) + + instance.authenticator().openLoginActivity(context)?.run() + finish() + } + } + .removingItems(state) + .build() + setContentView(LithoView.create(componentContext, component)) + } + + override fun onBackPressed() { + super.onBackPressed() + } + + override fun notifyThemeChange() { + setSystemTheme() + } +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt new file mode 100644 index 00000000..beac88d1 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt @@ -0,0 +1,91 @@ +package com.bijoysingh.quicknote.firebase.activity + +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.text.Layout +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.VerticalScroll +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.support.specs.color +import com.maubis.scarlet.base.support.ui.LithoCircleDrawable +import com.maubis.scarlet.base.support.ui.ThemeColorType + +@LayoutSpec +object FirebaseRemovalRootViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop removingItems: Boolean): Component { + val buttonTitle = when { + removingItems -> R.string.firebase_removal_page_clearing_button + else -> R.string.firebase_removal_page_clear_button + } + return Column.create(context) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(FirebaseRemovalContentView.create(context))) + .child(Row.create(context) + .backgroundRes(R.drawable.login_button_disabled) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.ic_google_icon) + .heightDip(36f) + ) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(FirebaseRemovalRootView.onLogoutClickEvent(context))) + .build() + } + + @OnEvent(ClickEvent::class) + fun onLogoutClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + +@LayoutSpec +object FirebaseRemovalContentViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext): Component { + return Column.create(context) + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.firebase_removal_page_login_title) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.firebase_removal_page_important_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(FirebaseIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_delete_permanently) + .titleRes(R.string.firebase_removal_page_remove_details)) + .child(FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_action_backup) + .titleRes(R.string.firebase_removal_page_next_details)) + .build() + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index 87a8f963..6eed26b9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -50,7 +50,7 @@ class ForgetMeActivity : ThemedActivity() { return@setOnClickListener } - val isLoggedIn = ApplicationBase.instance.authenticator().isLoggedIn() + val isLoggedIn = ApplicationBase.instance.authenticator().isLegacyLoggedIn() if (!isLoggedIn) { return@setOnClickListener } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt index c3c53a66..0cb74eac 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt @@ -1,12 +1,9 @@ package com.bijoysingh.quicknote.firebase.support import android.content.Context -import android.content.Intent import android.os.Handler import android.os.Looper import com.bijoysingh.quicknote.Scarlet.Companion.firebase -import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity -import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.initFirebaseDatabase import com.github.bijoysingh.starter.async.SimpleThreadExecutor import com.github.bijoysingh.starter.util.ToastHelper @@ -15,25 +12,27 @@ import com.google.firebase.FirebaseNetworkException import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.FirebaseDatabase import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean -class FirebaseAuthenticator() : IAuthenticator { +class FirebaseAuthenticator() { + var hasUidSetup: AtomicBoolean = AtomicBoolean(false) var userId: String? = null - override fun userId(context: Context): String? { + fun userId(context: Context): String? { val user = FirebaseAuth.getInstance().currentUser return user?.uid } - override fun setup(context: Context) { + fun setup(context: Context) { GlobalScope.launch { FirebaseApp.initializeApp(context) try { userId = userId(context) + hasUidSetup.set(true) if (userId === null) { return@launch } @@ -47,24 +46,22 @@ class FirebaseAuthenticator() : IAuthenticator { } } - override fun isLoggedIn(): Boolean { + fun isLoggedIn(): Boolean { + if (hasUidSetup.get()) { + return userId !== null + } + + userId = FirebaseAuth.getInstance().currentUser?.uid + hasUidSetup.set(true) return userId !== null } - override fun logout() { + fun logout() { userId = null FirebaseAuth.getInstance().signOut() firebase?.logout() } - override fun openLoginActivity(context: Context) = Runnable { - context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) - } - - override fun openForgetMeActivity(context: Context) = Runnable { - context.startActivity(Intent(context, ForgetMeActivity::class.java)) - } - private fun reloadUser(context: Context) { SimpleThreadExecutor.execute { try { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt index ec378deb..c24e3e7c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt @@ -67,7 +67,8 @@ fun FirebaseRemoteDatabase.setTagListener() { if (tag === null) { return } - onRemoteRemove(tag) + // TODO: This is disabled + // onRemoteRemove(tag) } catch (exception: Exception) { // Ignore if exception } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt deleted file mode 100644 index 83f95f8b..00000000 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/ScarletAuthenticator.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.bijoysingh.quicknote.firebase.support - -import android.content.Context -import com.bijoysingh.quicknote.Scarlet -import com.bijoysingh.quicknote.drive.GDriveAuthenticator -import com.maubis.scarlet.base.config.auth.IAuthenticator - -const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" -var sGDriveLoggedIn: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit - -const val KEY_FIREBASE_KILLED = "firebase_killed" -var sFirebaseKilled: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_FIREBASE_KILLED, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit - -class ScarletAuthenticator() : IAuthenticator { - - val firebase = FirebaseAuthenticator() - val gdrive = GDriveAuthenticator() - - override fun userId(context: Context): String? { - return firebase.userId(context) - } - - override fun setup(context: Context) { - firebase.setup(context) - } - - override fun isLoggedIn(): Boolean { - return firebase.isLoggedIn() - } - - override fun logout() { - firebase.logout() - } - - override fun openLoginActivity(context: Context) = gdrive.openLoginActivity(context) - - override fun openForgetMeActivity(context: Context) = firebase.openForgetMeActivity(context) -} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt new file mode 100644 index 00000000..0b9de2ea --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -0,0 +1,73 @@ +package com.bijoysingh.quicknote.scarlet + +import android.content.Context +import android.content.Intent +import com.bijoysingh.quicknote.Scarlet +import com.bijoysingh.quicknote.drive.GDriveAuthenticator +import com.bijoysingh.quicknote.drive.GDriveLoginActivity +import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity +import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity +import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator +import com.maubis.scarlet.base.config.auth.IAuthenticator + +const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" +var sGDriveLoggedIn: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit + +const val KEY_FIREBASE_KILLED = "firebase_killed" +var sFirebaseKilled: Boolean + get() = Scarlet.gDriveConfig?.get(KEY_FIREBASE_KILLED, false) ?: false + set(value) = Scarlet.gDriveConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit + +class ScarletAuthenticator() : IAuthenticator { + + val firebase = FirebaseAuthenticator() + val gdrive = GDriveAuthenticator() + + override fun userId(context: Context): String? { + if (sGDriveLoggedIn) { + return gdrive.userId(context) + } + return firebase.userId(context) + } + + override fun setup(context: Context) { + if (sGDriveLoggedIn) { + gdrive.setup(context) + return + } + firebase.setup(context) + } + + override fun isLoggedIn(context: Context): Boolean { + if (sGDriveLoggedIn) { + return gdrive.isLoggedIn(context) + } + return firebase.isLoggedIn() + } + + override fun isLegacyLoggedIn(): Boolean { + return firebase.isLoggedIn() + } + + override fun logout() { + if (sGDriveLoggedIn) { + gdrive.logout() + return + } + firebase.logout() + } + + override fun openLoginActivity(context: Context) = Runnable { + context.startActivity(Intent(context, GDriveLoginActivity::class.java)) + } + + override fun openForgetMeActivity(context: Context) = Runnable { + context.startActivity(Intent(context, ForgetMeActivity::class.java)) + } + + override fun openTransferDataActivity(context: Context) = Runnable { + context.startActivity(Intent(context, FirebaseRemovalActivity::class.java)) + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index e0c6c868..d4ed67a0 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -6,7 +6,6 @@ import com.bijoysingh.quicknote.BuildConfig import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.openIfNeeded import com.bijoysingh.quicknote.firebase.support.RemoteConfigFetcher -import com.bijoysingh.quicknote.firebase.support.ScarletAuthenticator import com.maubis.scarlet.base.config.MaterialNoteConfig import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher From 0f64e771f542f12b21eeef465d53c7c3f4483f9c Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 1 Jun 2019 19:06:22 +0100 Subject: [PATCH 028/134] Adding clearer details around transfer to Google Drive --- .../base/config/auth/IAuthenticator.kt | 2 + .../base/config/auth/NullAuthenticator.kt | 2 + .../sheet/SettingsOptionsBottomSheet.kt | 14 ++- base/src/main/res/values/strings.xml | 12 ++- scarlet/src/main/AndroidManifest.xml | 1 + .../quicknote/drive/GDriveActivitySpecs.kt | 2 + .../quicknote/drive/GDriveAuthenticator.kt | 6 +- .../quicknote/drive/GDriveLogoutActivity.kt | 96 +++++++++++++++++++ .../drive/GDriveLogoutActivitySpecs.kt | 81 ++++++++++++++++ .../quicknote/drive/GDriveRemoteDatabase.kt | 2 +- .../quicknote/drive/GDriveServiceHelper.kt | 9 +- .../activity/FirebaseRemovalActivity.kt | 2 + .../activity/FirebaseRemovalActivitySpecs.kt | 57 ++++++++--- .../quicknote/scarlet/ScarletAuthenticator.kt | 5 + 14 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index 7c1dc72b..5b225c88 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -18,5 +18,7 @@ interface IAuthenticator { fun openTransferDataActivity(context: Context): Runnable? + fun openLogoutActivity(context: Context): Runnable? + fun logout() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index d6dacce8..3ec3a330 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -9,6 +9,8 @@ class NullAuthenticator : IAuthenticator { override fun openTransferDataActivity(context: Context): Runnable? = null + override fun openLogoutActivity(context: Context): Runnable? = null + override fun logout() {} override fun setup(context: Context) {} diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index fd85b8ed..750430a2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -28,7 +28,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { val options = ArrayList() val loginClick = ApplicationBase.instance.authenticator().openLoginActivity(activity) - val firebaseLoggedIn = ApplicationBase.instance.authenticator().userId(activity) !== null + val isLoggedIn = ApplicationBase.instance.authenticator().isLoggedIn(activity) val migrateToPro = getMigrateToProAppInformationItem(activity) options.add(LithoOptionsItem( @@ -50,7 +50,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { loginClick?.run() dismiss() }, - visible = loginClick !== null && !firebaseLoggedIn + visible = loginClick !== null && !isLoggedIn )) options.add(LithoOptionsItem( title = R.string.home_option_ui_experience, @@ -132,10 +132,16 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.home_option_logout_of_app_subtitle, icon = R.drawable.ic_sign_in_options, listener = { - ApplicationBase.instance.authenticator().logout() + if (ApplicationBase.instance.authenticator().isLegacyLoggedIn()) { + ApplicationBase.instance.authenticator().logout() + dismiss() + return@LithoOptionsItem + } + + ApplicationBase.instance.authenticator().openLogoutActivity(activity)?.run() dismiss() }, - visible = firebaseLoggedIn + visible = isLoggedIn )) return options } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index d9705e46..6eb79f56 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -440,6 +440,11 @@ Sign In to Google Drive Signing In… + Sign Out of Scarlet + Scarlet will stop syncing your notes, tags and folders to your Google Drive. + Sign Out from Drive + Signing Out In… + Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. Your photos are uploaded and synced across devices as well. @@ -449,8 +454,8 @@ Signing In… We used to store your information on Google Firebase. After logging in we will recover these to your device - Changes to notes will NOT be synced over to Google Firebase, and you need to login on to Google Drive - Once your notes are recovered, we will delete them from Google Firebase, and then log you out + New notes and changes will NOT be synced over to Google Firebase, and you need to login on to Google Drive + Once your notes are recovered, you can delete them from Google Firebase, and then switch over Your data is not being synced! Data Sync based on legacy login is disabled, consider switching over to Google Drive based sync. @@ -459,6 +464,9 @@ Clear Data and Sign Out Clearing and Signing Out We used to store your information on Google Firebase. We are moving over to storing your notes in your Google Drive. + More Privacy. + Photo Sync. + Next Steps We will first delete your notes from Firebase and log you out. You can then log into Google Drive, and we will upload your data along with images. diff --git a/scarlet/src/main/AndroidManifest.xml b/scarlet/src/main/AndroidManifest.xml index 306f2b4b..1a857d1d 100644 --- a/scarlet/src/main/AndroidManifest.xml +++ b/scarlet/src/main/AndroidManifest.xml @@ -114,6 +114,7 @@ + R.string.google_drive_page_logging_out_button + else -> R.string.google_drive_page_logout_button + } + return Column.create(context) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(GDriveLogoutContentView.create(context))) + .child(Row.create(context) + .backgroundRes(R.drawable.login_button_disabled) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.gdrive_icon) + .heightDip(36f) + ) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(GDriveLogoutRootView.onLogoutClick(context))) + .build() + } + + @OnEvent(ClickEvent::class) + fun onLogoutClick(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + +@LayoutSpec +object GDriveLogoutContentViewSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext): Component { + return Column.create(context) + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.google_drive_page_logout_title) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.google_drive_page_logout_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .build() + } +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 5e10ae5e..80b1eb7c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -199,7 +199,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { fun logout() { GlobalScope.launch { reset() - gDriveDatabase?.drop() + // gDriveDatabase?.drop() gDriveConfig?.clearSync() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index b69d5dc3..76b82c4f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -27,6 +27,8 @@ const val GOOGLE_DRIVE_FILE_MIME_TYPE = "text/plain" const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" +const val MAX_THRESHOLD_QUERIES_PER_SECOND = 5 +const val MIN_RESET_QUERIES_PER_SECOND = 0.1 var lastCheckpointTime: AtomicLong = AtomicLong(0L) var numQueriesSinceLastCheckpoint: AtomicLong = AtomicLong(0L) @@ -40,15 +42,16 @@ class ErrorCallable(val action: String, val callable: Callable) : Callable val currentCount = numQueriesSinceLastCheckpoint.get() * 1.0 val deltaTimeS = (System.currentTimeMillis() - lastCheckpointTime.get()) / 1000.0 - log("GDrive", "Request being called: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=${currentCount / deltaTimeS}") + val currentQueriesPerSecond = (currentCount / deltaTimeS) + log("GDrive", "Request being called: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=$currentQueriesPerSecond") if (currentCount >= 10 && deltaTimeS > 0) { when { - (currentCount / deltaTimeS) > 0.9 -> { + currentQueriesPerSecond > MAX_THRESHOLD_QUERIES_PER_SECOND -> { log("GDrive", "Rate limiting measures taken: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS") SystemClock.sleep(500L) return call() } - (currentCount / deltaTimeS) < 0.1 -> { + (currentCount / deltaTimeS) < MIN_RESET_QUERIES_PER_SECOND -> { numQueriesSinceLastCheckpoint.set(0L) lastCheckpointTime.set(SystemClock.currentThreadTimeMillis()) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt index ab30fd12..6e61c7a9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt @@ -7,6 +7,7 @@ import android.util.Log import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy import com.bijoysingh.quicknote.firebase.initFirebaseDatabase +import com.bijoysingh.quicknote.scarlet.sFirebaseKilled import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView @@ -59,6 +60,7 @@ class FirebaseRemovalActivity : ThemedActivity() { instance.authenticator().logout() setButton(false) + sFirebaseKilled = true instance.authenticator().openLoginActivity(context)?.run() finish() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt index beac88d1..aea13938 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt @@ -1,10 +1,12 @@ package com.bijoysingh.quicknote.firebase.activity -import android.graphics.Color -import android.graphics.drawable.Drawable import android.text.Layout +import com.bijoysingh.quicknote.drive.GDriveRootView import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop import com.facebook.litho.widget.Image import com.facebook.litho.widget.Text import com.facebook.litho.widget.VerticalScroll @@ -13,8 +15,6 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.support.specs.color -import com.maubis.scarlet.base.support.ui.LithoCircleDrawable import com.maubis.scarlet.base.support.ui.ThemeColorType @LayoutSpec @@ -77,15 +77,46 @@ object FirebaseRemovalContentViewSpec { .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_removal_page_important_details) .typeface(CoreConfig.FONT_MONSERRAT)) - .child(FirebaseIconView.create(context) + .child(Row.create(context) .marginDip(YogaEdge.TOP, 24f) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_delete_permanently) - .titleRes(R.string.firebase_removal_page_remove_details)) - .child(FirebaseIconView.create(context) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_action_backup) - .titleRes(R.string.firebase_removal_page_next_details)) + .child( + FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_privacy_policy) + .titleRes(R.string.firebase_removal_page_more_privacy_details) + .flexGrow(1f)) + .child(FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_image_gallery) + .titleRes(R.string.firebase_removal_page_photo_upload_details) + .flexGrow(1f)) + ) + .child(Text.create(context) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.BOTTOM, 10f) + .paddingDip(YogaEdge.TOP, 20f) + .textSizeRes(R.dimen.font_size_normal) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.firebase_removal_page_whats_next_details) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child(Text.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .marginDip(YogaEdge.VERTICAL, 4f) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.ALL, 12f) + .textSizeRes(R.dimen.font_size_normal) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.firebase_removal_page_remove_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(Text.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .marginDip(YogaEdge.VERTICAL, 4f) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.ALL, 12f) + .textSizeRes(R.dimen.font_size_normal) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.firebase_removal_page_next_details) + .typeface(CoreConfig.FONT_MONSERRAT)) .build() } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 0b9de2ea..957a7a90 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -5,6 +5,7 @@ import android.content.Intent import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.drive.GDriveAuthenticator import com.bijoysingh.quicknote.drive.GDriveLoginActivity +import com.bijoysingh.quicknote.drive.GDriveLogoutActivity import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator @@ -70,4 +71,8 @@ class ScarletAuthenticator() : IAuthenticator { override fun openTransferDataActivity(context: Context) = Runnable { context.startActivity(Intent(context, FirebaseRemovalActivity::class.java)) } + + override fun openLogoutActivity(context: Context) = Runnable { + context.startActivity(Intent(context, GDriveLogoutActivity::class.java)) + } } \ No newline at end of file From f8d59065ffc2ea5fcd0459c1627861e3d6ec05b9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 1 Jun 2019 20:15:40 +0100 Subject: [PATCH 029/134] [GDrive] Cleaning and de-duplicating logic in RemoteDB implementation --- .../base/export/data/ExportableData.kt | 100 ++++ .../base/export/data/ExportableExtensions.kt | 31 ++ .../base/export/data/ExportableFolder.kt | 26 - .../scarlet/base/export/data/ExportableTag.kt | 43 -- .../activity/OpenTextIntentOrFileActivity.kt | 9 +- .../base/main/sheets/AlertBottomSheet.kt | 1 - .../sheet/ThemeColorPickerBottomSheet.kt | 4 +- .../base/widget/AllNotesWidgetService.kt | 1 - .../quicknote/database/GDriveUploadData.kt | 1 + .../quicknote/drive/GDriveRemoteDatabase.kt | 484 +++++++----------- .../quicknote/firebase/data/FirebaseFolder.kt | 1 + .../quicknote/firebase/data/FirebaseNote.kt | 1 + .../quicknote/firebase/data/FirebaseTag.kt | 1 + .../firebase/data/FolderExtensions.kt | 1 + .../quicknote/firebase/data/NoteExtensions.kt | 1 + .../quicknote/firebase/data/TagExtensions.kt | 2 +- .../quicknote/scarlet/ScarletFolderActor.kt | 1 - 17 files changed, 335 insertions(+), 373 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt delete mode 100644 base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFolder.kt delete mode 100644 base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt new file mode 100644 index 00000000..41948ad0 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt @@ -0,0 +1,100 @@ +package com.maubis.scarlet.base.export.data + +import com.maubis.scarlet.base.core.folder.IFolderContainer +import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.core.note.generateUUID +import com.maubis.scarlet.base.core.tag.ITagContainer +import com.maubis.scarlet.base.core.tag.TagBuilder +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.tag.Tag +import org.json.JSONObject +import java.io.Serializable +import java.util.* + +class ExportableNoteMeta( + val uuid: String, + val timestamp: Long, + val updateTimestamp: Long, + val color: Int, + val state: String, + val tags: String, + val locked: Boolean, + val pinned: Boolean, + val folder: String) { + + constructor() : this( + "invalid", + Calendar.getInstance().timeInMillis, + Calendar.getInstance().timeInMillis, + -0xff8695, + NoteState.DEFAULT.name, + "", + false, + false, + "") +} + +class ExportableTag( + var uuid: String, + var title: String +) : Serializable, ITagContainer { + + override fun title(): String = title + + override fun uuid(): String = uuid + + constructor() : this("invalid", "") + + constructor(tag: Tag) : this( + tag.uuid, + tag.title + ) + + companion object { + fun fromJSON(json: JSONObject): ExportableTag { + val version = if (json.has("version")) json.getInt("version") else 1 + return when (version) { + 1 -> fromJSONObjectV1(json) + else -> fromJSONObjectV1(json) + } + } + + fun fromJSONObjectV1(json: JSONObject): ExportableTag { + return ExportableTag( + generateUUID(), + json["title"] as String) + } + + fun getBestPossibleTagObject(json: JSONObject): Tag { + return TagBuilder().copy(fromJSON(json)) + } + } +} + +class ExportableFolder( + val uuid: String, + val title: String, + val timestamp: Long, + val updateTimestamp: Long, + val color: Int +) : Serializable, IFolderContainer { + override fun timestamp(): Long = timestamp + override fun updateTimestamp(): Long = updateTimestamp + override fun color(): Int = color + override fun title(): String = title + override fun uuid(): String = uuid + + constructor(folder: Folder) : this( + folder.uuid ?: "", + folder.title ?: "", + folder.timestamp ?: 0, + folder.updateTimestamp, + folder.color ?: 0) + + constructor() : this( + "invalid", + "", + Calendar.getInstance().timeInMillis, + Calendar.getInstance().timeInMillis, + -0xff8695) +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt new file mode 100644 index 00000000..8d5eccfb --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt @@ -0,0 +1,31 @@ +package com.maubis.scarlet.base.export.data + +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.database.room.tag.Tag + +fun Note.getExportableNoteMeta(): ExportableNoteMeta { + return ExportableNoteMeta( + uuid, + timestamp, + updateTimestamp, + color, + state, + if (tags == null) "" else tags, + locked, + pinned, + folder + ) +} + +fun Folder.getExportableFolder(): ExportableFolder { + return ExportableFolder( + uuid, + title, + timestamp, + updateTimestamp, + color + ) +} + +fun Tag.getExportableTag(): ExportableTag = ExportableTag(uuid, title) \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFolder.kt deleted file mode 100644 index 006e2233..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFolder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.maubis.scarlet.base.export.data - -import com.maubis.scarlet.base.core.folder.IFolderContainer -import com.maubis.scarlet.base.database.room.folder.Folder -import java.io.Serializable - -class ExportableFolder( - val uuid: String, - val title: String, - val timestamp: Long, - val updateTimestamp: Long, - val color: Int -) : Serializable, IFolderContainer { - override fun timestamp(): Long = timestamp - override fun updateTimestamp(): Long = updateTimestamp - override fun color(): Int = color - override fun title(): String = title - override fun uuid(): String = uuid - - constructor(folder: Folder) : this( - folder.uuid ?: "", - folder.title ?: "", - folder.timestamp ?: 0, - folder.updateTimestamp, - folder.color ?: 0) -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt deleted file mode 100644 index 3893ff5a..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableTag.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.maubis.scarlet.base.export.data - -import com.maubis.scarlet.base.core.note.generateUUID -import com.maubis.scarlet.base.core.tag.ITagContainer -import com.maubis.scarlet.base.core.tag.TagBuilder -import com.maubis.scarlet.base.database.room.tag.Tag -import org.json.JSONObject -import java.io.Serializable - -class ExportableTag( - var uuid: String, - var title: String -) : Serializable, ITagContainer { - - override fun title(): String = title - - override fun uuid(): String = uuid - - constructor(tag: Tag) : this( - tag.uuid, - tag.title - ) - - companion object { - fun fromJSON(json: JSONObject): ExportableTag { - val version = if (json.has("version")) json.getInt("version") else 1 - return when (version) { - 1 -> fromJSONObjectV1(json) - else -> fromJSONObjectV1(json) - } - } - - fun fromJSONObjectV1(json: JSONObject): ExportableTag { - return ExportableTag( - generateUUID(), - json["title"] as String) - } - - fun getBestPossibleTagObject(json: JSONObject): Tag { - return TagBuilder().copy(fromJSON(json)) - } - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 70e23a12..99e9d825 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -156,11 +156,16 @@ class OpenTextIntentOrFileActivity : ThemedActivity() { fun handleFileIntent(intent: Intent): Boolean { val data = intent.data + val lastPathSegment = data?.lastPathSegment + if (data === null || lastPathSegment === null) { + return false + } + try { val inputStream = contentResolver.openInputStream(data) contentText = NoteImporter().readFileInputStream(InputStreamReader(inputStream)) - filenameText = data.lastPathSegment - inputStream.close() + filenameText = lastPathSegment + inputStream?.close() return true } catch (exception: Exception) { return false diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt index ca42c849..37cdcddd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt @@ -49,7 +49,6 @@ class AlertBottomSheet : LithoBottomSheet() { var config: AlertSheetConfig = AlertSheetConfig() override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { - val activity = context as ThemedActivity val component = Column.create(componentContext) .widthPercent(100f) .paddingDip(YogaEdge.VERTICAL, 8f) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index d83937cd..b8a2e1e0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -101,8 +101,8 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { .theme(theme) .isDisabled(disabled) .isSelected(theme.name == getThemeFromStore().name) - .onThemeSelected { theme -> - onThemeChange(theme) + .onThemeSelected { newTheme -> + onThemeChange(newTheme) reset(componentContext.androidContext, dialog) } .flexGrow(1f)) diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt index c91d41b0..530340b8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt @@ -52,7 +52,6 @@ class AllNotesRemoteViewsFactory(val context: Context) : RemoteViewsService.Remo val note = notes[position] - val intent = ViewAdvancedNoteActivity.getIntent(context, note) val views = RemoteViews(context.getPackageName(), R.layout.item_widget_note) views.setTextViewText(R.id.description, getWidgetNoteText(note)) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index f58741c5..7ef703be 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -4,6 +4,7 @@ import android.arch.persistence.room.* enum class GDriveDataType { NOTE, + NOTE_META, TAG, FOLDER, IMAGE, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 80b1eb7c..5d6714d5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,29 +1,28 @@ package com.bijoysingh.quicknote.drive import android.content.Context -import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.bijoysingh.quicknote.database.genGDriveUploadDatabase -import com.bijoysingh.quicknote.firebase.data.* -import com.github.bijoysingh.starter.prefs.Store +import com.bijoysingh.quicknote.firebase.data.FirebaseNote +import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.core.folder.IFolderContainer import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.INoteContainer -import com.maubis.scarlet.base.core.tag.ITagContainer import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils +import com.maubis.scarlet.base.export.data.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference const val FOLDER_NAME_IMAGES = "images" const val FOLDER_NAME_NOTES = "notes" +const val FOLDER_NAME_NOTES_META = "notes_meta" const val FOLDER_NAME_TAGS = "tags" const val FOLDER_NAME_FOLDERS = "folders" const val FOLDER_NAME_DELETED_NOTES = "deleted_notes" @@ -31,9 +30,13 @@ const val FOLDER_NAME_DELETED_TAGS = "deleted_tags" const val FOLDER_NAME_DELETED_FOLDERS = "deleted_folders" const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META = "g_drive_first_time_sync_note_meta" const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" +var sGDriveFirstSyncNoteMeta: Boolean + get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META, false) ?: false + set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META, value) ?: Unit var sGDriveFirstSyncNote: Boolean get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) ?: false set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) ?: Unit @@ -72,8 +75,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var driveHelper: GDriveServiceHelper? = null private var notesSync: GDriveRemoteFolder? = null - private var foldersSync: GDriveRemoteFolder? = null - private var tagsSync: GDriveRemoteFolder? = null + private var foldersSync: GDriveRemoteFolder? = null + private var tagsSync: GDriveRemoteFolder? = null private var imageSync: GDriveRemoteImageFolder? = null fun init(helper: GDriveServiceHelper) { @@ -90,10 +93,10 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() } tagsSync = GDriveRemoteFolder(GDriveDataType.TAG, gDriveDatabase!!, helper) { - ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getFirebaseTag() + ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() } foldersSync = GDriveRemoteFolder(GDriveDataType.FOLDER, gDriveDatabase!!, helper) { - ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getFirebaseFolder() + ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() } imageSync = GDriveRemoteImageFolder(GDriveDataType.IMAGE, gDriveDatabase!!, helper) @@ -116,29 +119,54 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } - fun initSubRootFolder(folderName: String, folderId: String) { + fun reset() { + isValidController = false + driveHelper = null + notesSync = null + foldersSync = null + tagsSync = null + imageSync = null + } + + fun logout() { + GlobalScope.launch { + reset() + gDriveConfig?.clearSync() + } + } + + /** + * Initialisation Methods + */ + private fun initSubRootFolder(folderName: String, folderId: String) { when (folderName) { FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNote) { - GlobalScope.launch { resyncNotesSync { } } + GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) } sGDriveFirstSyncNote = true } } + FOLDER_NAME_NOTES_META -> notesSync?.initContentFolderId(folderId) { + if (!sGDriveFirstSyncNoteMeta) { + GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } + sGDriveFirstSyncNoteMeta = true + } + } FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncTag) { - GlobalScope.launch { resyncTagsSync { } } + GlobalScope.launch { resyncDataSync(GDriveDataType.TAG) } sGDriveFirstSyncTag = true } } FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncFolder) { - GlobalScope.launch { resyncFoldersSync { } } + GlobalScope.launch { resyncDataSync(GDriveDataType.FOLDER) } sGDriveFirstSyncFolder = true } } FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncImage) { - GlobalScope.launch { resyncImagessSync { } } + GlobalScope.launch { resyncDataSync(GDriveDataType.IMAGE) } sGDriveFirstSyncImage = true } } @@ -148,12 +176,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } - fun onRootFolderLoaded(rootFolderId: String) { - createFolders(rootFolderId, listOf(FOLDER_NAME_IMAGES, FOLDER_NAME_NOTES, FOLDER_NAME_TAGS, FOLDER_NAME_FOLDERS)) + private fun onRootFolderLoaded(rootFolderId: String) { + createFolders(rootFolderId, listOf(FOLDER_NAME_IMAGES, FOLDER_NAME_NOTES, FOLDER_NAME_TAGS, FOLDER_NAME_FOLDERS, FOLDER_NAME_NOTES_META)) createFolders(rootFolderId, listOf(FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS)) } - fun createFolders(rootFolderId: String, expectedFolders: List) { + private fun createFolders(rootFolderId: String, expectedFolders: List) { val knownFolderIds = expectedFolders.filter { folderIdForFolderName(it).isNotEmpty() } knownFolderIds.forEach { GlobalScope.launch { initSubRootFolder(it, folderIdForFolderName(it)) } @@ -187,33 +215,23 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } - fun reset() { - isValidController = false - driveHelper = null - notesSync = null - foldersSync = null - tagsSync = null - imageSync = null - } - - fun logout() { - GlobalScope.launch { - reset() - // gDriveDatabase?.drop() - gDriveConfig?.clearSync() - } - } - - private fun deleteEverything() { + /** + * Notify local changes to the notes + */ + fun notifyInsert(data: Any) { if (!isValidController) { return } - } - fun notifyInsert(note: INoteContainer) { - if (!isValidController || note !is FirebaseNote) { - return + when { + data is ExportableTag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid) + data is ExportableFolder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + data is FirebaseNote -> notifyInsertImpl(data) + data is ExportableNoteMeta -> localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) } + } + + fun notifyInsertImpl(note: FirebaseNote) { localDatabaseUpdate(GDriveDataType.NOTE, note.uuid) val database = gDriveDatabase @@ -254,138 +272,64 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } - fun notifyInsert(tag: ITagContainer) { - if (!isValidController || tag !is FirebaseTag) { + fun notifyRemove(data: Any) { + if (!isValidController) { return } - localDatabaseUpdate(GDriveDataType.TAG, tag.uuid) - } - fun notifyInsert(folder: IFolderContainer) { - if (!isValidController || folder !is FirebaseFolder) { - return + when { + data is ExportableTag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, true) + data is ExportableFolder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, true) + data is FirebaseNote -> localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, true) + data is ExportableNoteMeta -> localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, true) } - localDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid) } - fun notifyRemove(note: INoteContainer) { - if (!isValidController || note !is FirebaseNote) { - return - } - localDatabaseUpdate(GDriveDataType.NOTE, note.uuid, true) - } + /** + * Resync Functions + */ - fun notifyRemove(tag: ITagContainer) { - if (!isValidController || tag !is FirebaseTag) { + @Synchronized + fun resync(force: Boolean, onSyncCompleted: () -> Unit) { + if (!isValidController) { + onSyncCompleted() return } - localDatabaseUpdate(GDriveDataType.TAG, tag.uuid, true) - } - fun notifyRemove(folder: IFolderContainer) { - if (!isValidController || folder !is FirebaseFolder) { + if (!force && sGDriveLastSync > getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { + onSyncCompleted() return } - localDatabaseUpdate(GDriveDataType.FOLDER, folder.uuid, true) - } - - fun resyncNotesSync(onSyncCompleted: () -> Unit) { - gDriveDatabase?.getByType(GDriveDataType.NOTE.name)?.forEach { - val sameDelete = it.localStateDeleted == it.gDriveStateDeleted - val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp - if (!sameUpdateTime) { - when { - sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - ApplicationBase.instance.notesDatabase().getByUUID(it.uuid)?.getFirebaseNote()?.apply { - insert(this) - } - sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertNote(it) - !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeNote(it.uuid) - !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveNote(it) - } - } - } - onSyncCompleted() - } - - fun resyncTagsSync(onSyncCompleted: () -> Unit) { - gDriveDatabase?.getByType(GDriveDataType.TAG.name)?.forEach { - val sameDelete = it.localStateDeleted == it.gDriveStateDeleted - val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp - if (!sameUpdateTime) { - when { - sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - ApplicationBase.instance.tagsDatabase().getByUUID(it.uuid)?.getFirebaseTag()?.apply { - insert(this) - } - sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertTag(it) - !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeTag(it.uuid) - !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveTag(it) - } - } - } - onSyncCompleted() - } + sGDriveLastSync = getTrueCurrentTime() - fun resyncFoldersSync(onSyncCompleted: () -> Unit) { - gDriveDatabase?.getByType(GDriveDataType.FOLDER.name)?.forEach { - val sameDelete = it.localStateDeleted == it.gDriveStateDeleted - val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp - if (!sameUpdateTime) { - when { - sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - ApplicationBase.instance.foldersDatabase().getByUUID(it.uuid)?.getFirebaseFolder()?.apply { - insert(this) - } - sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertFolder(it) - !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeFolder(it.uuid) - !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveFolder(it) - } - } + GlobalScope.launch { + resyncDataSync(GDriveDataType.NOTE) + resyncDataSync(GDriveDataType.NOTE_META) + resyncDataSync(GDriveDataType.TAG) + resyncDataSync(GDriveDataType.FOLDER) + resyncDataSync(GDriveDataType.IMAGE) + onSyncCompleted() } - onSyncCompleted() } - fun resyncImagessSync(onSyncCompleted: () -> Unit) { - gDriveDatabase?.getByType(GDriveDataType.IMAGE.name)?.forEach { + fun resyncDataSync(type: GDriveDataType) { + gDriveDatabase?.getByType(type.name)?.forEach { val sameDelete = it.localStateDeleted == it.gDriveStateDeleted val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp if (!sameUpdateTime) { when { - sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> - toImageUUID(it.uuid)?.apply { - insert(this) - } - sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsertImage(it) - !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> removeImage(it.uuid) - !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemoveImage(it) + sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> insert(type, it) + sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsert(type, it) + !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> remove(type, it) + !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemove(type, it) } } } - onSyncCompleted() } - @Synchronized - fun resync(force: Boolean, onSyncCompleted: () -> Unit) { - if (!isValidController) { - onSyncCompleted() - return - } - - if (!force && sGDriveLastSync > getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { - onSyncCompleted() - return - } - sGDriveLastSync = getTrueCurrentTime() - - GlobalScope.launch { - resyncNotesSync {} - resyncTagsSync {} - resyncFoldersSync {} - resyncImagessSync {} - onSyncCompleted() - } - } + /** + * Update the database about information + */ private fun localDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, removed: Boolean = false) { GlobalScope.launch { @@ -423,126 +367,61 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } - private fun insert(note: INoteContainer) { - if (!isValidController || note !is FirebaseNote) { - return - } - notesSync?.insert(note.uuid, note) - } - - private fun insert(imageUUID: ImageUUID) { - if (!isValidController) { - return - } - imageSync?.insert(imageUUID) - } - - private fun insert(tag: ITagContainer) { - if (!isValidController || tag !is FirebaseTag) { - return - } - tagsSync?.insert(tag.uuid, tag) - } - - private fun insert(folder: IFolderContainer) { - if (!isValidController || folder !is FirebaseFolder) { - return - } - foldersSync?.insert(folder.uuid, folder) - } + /** + * Core Data Functions + */ - private fun removeNote(uuid: String) { + private fun insert(type: GDriveDataType, data: GDriveUploadData) { if (!isValidController) { return } - notesSync?.delete(uuid) - } - - private fun removeTag(uuid: String) { - if (!isValidController) { - return - } - tagsSync?.delete(uuid) - } - private fun removeFolder(uuid: String) { - if (!isValidController) { - return - } - foldersSync?.delete(uuid) - } - - private fun removeImage(uuid: String) { - if (!isValidController) { - return - } - val imageUUID = toImageUUID(uuid) - if (imageUUID !== null) { - imageSync?.delete(imageUUID) - } - } - - private fun onRemoteInsertNote(data: GDriveUploadData) { - if (!isValidController) { - return - } - - val context = weakContext.get() - if (context === null) { - return - } - - onRemoteInsert(data.fileId) { - try { - val item = Gson().fromJson(it, FirebaseNote::class.java) - IRemoteDatabaseUtils.onRemoteInsert(context, item) - remoteDatabaseUpdate(GDriveDataType.NOTE, item.uuid) - } catch (exception: Exception) { + when (type) { + GDriveDataType.NOTE -> { + ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getFirebaseNote()?.apply { + notesSync?.insert(this.uuid, this) + } } - } - } - - private fun onRemoteInsertTag(data: GDriveUploadData) { - if (!isValidController) { - return - } - - val context = weakContext.get() - if (context === null) { - return - } - - onRemoteInsert(data.fileId) { - try { - val item = Gson().fromJson(it, FirebaseTag::class.java) - IRemoteDatabaseUtils.onRemoteInsert(context, item) - remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) - } catch (exception: Exception) { + GDriveDataType.NOTE_META -> TODO() + GDriveDataType.TAG -> { + ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { + tagsSync?.insert(this.uuid, this) + } + } + GDriveDataType.FOLDER -> { + ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { + foldersSync?.insert(this.uuid, this) + } + } + GDriveDataType.IMAGE -> { + toImageUUID(data.uuid)?.apply { + imageSync?.insert(this) + } } } } - private fun onRemoteInsertFolder(data: GDriveUploadData) { + private fun remove(type: GDriveDataType, data: GDriveUploadData) { if (!isValidController) { return } - val context = weakContext.get() - if (context === null) { - return - } - - onRemoteInsert(data.fileId) { - try { - val item = Gson().fromJson(it, FirebaseFolder::class.java) - IRemoteDatabaseUtils.onRemoteInsert(context, item) - remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) - } catch (exception: Exception) { + val uuid = data.uuid + when (type) { + GDriveDataType.NOTE -> notesSync?.delete(uuid) + GDriveDataType.NOTE_META -> TODO() + GDriveDataType.TAG -> tagsSync?.delete(uuid) + GDriveDataType.FOLDER -> foldersSync?.delete(uuid) + GDriveDataType.IMAGE -> { + val imageUUID = toImageUUID(uuid) + if (imageUUID !== null) { + imageSync?.delete(imageUUID) + } } } } - private fun onRemoteInsertImage(data: GDriveUploadData) { + private fun onRemoteInsert(type: GDriveDataType, data: GDriveUploadData) { if (!isValidController) { return } @@ -552,34 +431,56 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } - val imageUUID = toImageUUID(data.uuid) - if (imageUUID !== null) { - val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) - if (imageFile.exists()) { - remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) - return + when (type) { + GDriveDataType.NOTE -> { + onRemoteInsertImpl(data.fileId) { + try { + val item = Gson().fromJson(it, FirebaseNote::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseUpdate(GDriveDataType.NOTE, item.uuid) + } catch (exception: Exception) { + } + } } - - driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { - remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + GDriveDataType.NOTE_META -> TODO() + GDriveDataType.TAG -> { + onRemoteInsertImpl(data.fileId) { + try { + val item = Gson().fromJson(it, ExportableTag::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) + } catch (exception: Exception) { + } + } } - } - } - - private fun onRemoteRemoveNote(data: GDriveUploadData) { - if (!isValidController) { - return - } + GDriveDataType.FOLDER -> { + onRemoteInsertImpl(data.fileId) { + try { + val item = Gson().fromJson(it, ExportableFolder::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + } catch (exception: Exception) { + } + } + } + GDriveDataType.IMAGE -> { + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null) { + val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + if (imageFile.exists()) { + remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + return + } - val context = weakContext.get() - if (context === null) { - return + driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { + remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + } + } + } } - IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) - remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) } - private fun onRemoteRemoveTag(data: GDriveUploadData) { + private fun onRemoteRemove(type: GDriveDataType, data: GDriveUploadData) { if (!isValidController) { return } @@ -588,37 +489,28 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (context === null) { return } - IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) - remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) - } - private fun onRemoteRemoveFolder(data: GDriveUploadData) { - if (!isValidController) { - return - } - - val context = weakContext.get() - if (context === null) { - return + when (type) { + GDriveDataType.NOTE -> IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) + GDriveDataType.NOTE_META -> TODO() + GDriveDataType.TAG -> IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) + GDriveDataType.FOLDER -> IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) + GDriveDataType.IMAGE -> { + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null) { + val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + imageFile.delete() + } + } } - IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) - remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + remoteDatabaseUpdate(type, data.uuid) } - private fun onRemoteRemoveImage(data: GDriveUploadData) { - if (!isValidController) { - return - } - - val imageUUID = toImageUUID(data.uuid) - if (imageUUID !== null) { - val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) - imageFile.delete() - remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) - } - } + /** + * Additional internal methods + */ - private fun onRemoteInsert(fileId: String, onDataAvailable: (String) -> Unit) { + private fun onRemoteInsertImpl(fileId: String, onDataAvailable: (String) -> Unit) { driveHelper?.readFile(fileId)?.addOnCompleteListener { val data = it.result if (data !== null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt index 6f8a4367..1e68b10d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt @@ -4,6 +4,7 @@ import com.google.firebase.database.Exclude import com.maubis.scarlet.base.core.folder.IFolderContainer import java.util.* +// TODO: Remove this on Firebase deprecation class FirebaseFolder( val uuid: String, val title: String, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt index 41ec088e..b61dcdd8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt @@ -5,6 +5,7 @@ import com.maubis.scarlet.base.core.note.INoteContainer import com.maubis.scarlet.base.core.note.NoteState import java.util.* +// TODO: Remove this on Firebase deprecation class FirebaseNote( val uuid: String, val description: String, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt index 95eda191..dae97e72 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt @@ -3,6 +3,7 @@ package com.bijoysingh.quicknote.firebase.data import com.google.firebase.database.Exclude import com.maubis.scarlet.base.core.tag.ITagContainer +// TODO: Remove this on Firebase deprecation class FirebaseTag( val uuid: String, val title: String) : ITagContainer { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt index a13fcf42..84c7849d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.firebase.data import com.maubis.scarlet.base.database.room.folder.Folder +// TODO: Remove this on Firebase deprecation fun Folder.getFirebaseFolder(): FirebaseFolder { return FirebaseFolder( uuid, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt index 1f06d06d..78132758 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.firebase.data import com.maubis.scarlet.base.database.room.note.Note +// TODO: Remove this on Firebase deprecation fun Note.getFirebaseNote(): FirebaseNote { return FirebaseNote( uuid, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/TagExtensions.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/TagExtensions.kt index c335f400..52b86574 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/TagExtensions.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/TagExtensions.kt @@ -2,5 +2,5 @@ package com.bijoysingh.quicknote.firebase.data import com.maubis.scarlet.base.database.room.tag.Tag - +// TODO: Remove this on Firebase deprecation fun Tag.getFirebaseTag(): FirebaseTag = FirebaseTag(uuid, title) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 4ea678d7..781510bf 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -1,6 +1,5 @@ package com.bijoysingh.quicknote.scarlet -import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.firebase.data.getFirebaseFolder import com.maubis.scarlet.base.core.folder.MaterialFolderActor From eaec5d584a0ecbe83cf3d99229216ef56606d7c4 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 1 Jun 2019 21:48:40 +0100 Subject: [PATCH 030/134] [GDrive] Adding Logic to Upload in User Editable Format --- .../scarlet/base/core/format/FormatBuilder.kt | 41 +----- .../base/export/data/ExportableData.kt | 26 ++++ .../base/export/data/ExportableExtensions.kt | 65 +++++++++ .../base/export/support/NoteExporter.kt | 8 +- .../scarlet/base/note/MarkdownExtensions.kt | 58 ++++++++ .../main/java/com/maubis/markdown/Markdown.kt | 6 - .../markdown/segmenter/MarkdownSegment.kt | 9 ++ .../quicknote/drive/GDriveRemoteDatabase.kt | 136 +++++++++++++----- .../quicknote/drive/GDriveRemoteFolder.kt | 18 ++- .../quicknote/scarlet/ScarletFolderActor.kt | 9 +- .../quicknote/scarlet/ScarletNoteActor.kt | 8 +- .../quicknote/scarlet/ScarletTagActor.kt | 10 +- 12 files changed, 284 insertions(+), 110 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt index f7321436..f4b86b23 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.core.format -import com.maubis.markdown.segmenter.TextSegmenter -import com.maubis.scarlet.base.note.toRawFormat +import com.maubis.markdown.segmenter.MarkdownSegmentType +import com.maubis.scarlet.base.note.toInternalFormats import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -38,41 +38,14 @@ class FormatBuilder { continue } - val moreFormats = TextSegmenter(format.text).get().map { it.toRawFormat() } - var lastFormat: Format? = null - for (rawFormat in moreFormats) { - val isCheckedType = (rawFormat.formatType == FormatType.CHECKLIST_UNCHECKED || rawFormat.formatType == FormatType.CHECKLIST_CHECKED) - if (lastFormat != null && !isCheckedType) { - lastFormat.text += "\n" - lastFormat.text += rawFormat.text - continue - } - - if (!isCheckedType) { - rawFormat.formatType = FormatType.TEXT - lastFormat = rawFormat - continue - } - - if (lastFormat != null) { - extractedFormats.add(lastFormat) - lastFormat = null - } - - rawFormat.text = rawFormat.text - .removePrefix("[] ") - .removePrefix("[x] ") - .removePrefix("[ ] ") - .removePrefix("[X] ") - extractedFormats.add(rawFormat) - } - if (lastFormat !== null) { - extractedFormats.add(lastFormat) - } - + val moreFormats = format.text.toInternalFormats(arrayOf( + MarkdownSegmentType.CHECKLIST_CHECKED, + MarkdownSegmentType.CHECKLIST_UNCHECKED)) + extractedFormats.addAll(moreFormats) } return getDescription(extractedFormats) } + fun getFormats(note: String): List { val formats = ArrayList() try { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt index 41948ad0..d5a4febb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt @@ -11,6 +11,23 @@ import org.json.JSONObject import java.io.Serializable import java.util.* +/** + * Data class containing the note as a string of the content which can be stored in user + * readable format (markdown) and meta data object + */ +class ExportableSplitNote( + val content: String, + val meta: ExportableNoteMeta) { + + // Default failsafe constructor for Gson to use + constructor() : this( + "", + ExportableNoteMeta()) +} + +/** + * Data class containing only the meta data for the note which makes it unique to Scarlet + */ class ExportableNoteMeta( val uuid: String, val timestamp: Long, @@ -22,6 +39,7 @@ class ExportableNoteMeta( val pinned: Boolean, val folder: String) { + // Default failsafe constructor for Gson to use constructor() : this( "invalid", Calendar.getInstance().timeInMillis, @@ -34,6 +52,9 @@ class ExportableNoteMeta( "") } +/** + * Data class for the exportability of tags + */ class ExportableTag( var uuid: String, var title: String @@ -43,6 +64,7 @@ class ExportableTag( override fun uuid(): String = uuid + // Default failsafe constructor for Gson to use constructor() : this("invalid", "") constructor(tag: Tag) : this( @@ -71,6 +93,9 @@ class ExportableTag( } } +/** + * Data class for the exportability of folder + */ class ExportableFolder( val uuid: String, val title: String, @@ -91,6 +116,7 @@ class ExportableFolder( folder.updateTimestamp, folder.color ?: 0) + // Default failsafe constructor for Gson to use constructor() : this( "invalid", "", diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt index 8d5eccfb..fa77de70 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt @@ -1,8 +1,60 @@ package com.maubis.scarlet.base.export.data +import com.maubis.markdown.segmenter.MarkdownSegmentType +import com.maubis.markdown.segmenter.TextSegmenter +import com.maubis.scarlet.base.core.format.Format +import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.note.toInternalFormats + +/** + * Converts the note which is markdown into internal format + */ +fun fromExportedMarkdown(content: String): String { + val formats = content.toInternalFormats() + return FormatBuilder().getDescription(formats) +} + +/** + * Converts the note's internal description format into markdown which can be used to export. + */ +fun Note.toExportedMarkdown(): String { + val markdownBuilder = StringBuilder() + getFormats().forEach { format -> + val text = format.text + val formatMarkdown = when (format.formatType) { + FormatType.NUMBERED_LIST -> "- $text" + FormatType.HEADING -> "# $text" + FormatType.CHECKLIST_CHECKED -> "[x] $text" + FormatType.CHECKLIST_UNCHECKED -> "[ ] $text" + FormatType.SUB_HEADING -> "## $text" + FormatType.CODE -> "```\n$text\n```" + FormatType.QUOTE -> "> $text" + // TODO: Fix the fact that markdown parsing wont parse this correctly + FormatType.IMAGE -> "$text" + FormatType.SEPARATOR -> "\n---\n" + FormatType.TEXT -> text + + // NOTE: All the following states should never happen at this place + FormatType.HEADING_3 -> text + FormatType.TAG -> "" + FormatType.EMPTY -> "" + } + markdownBuilder.append(formatMarkdown) + markdownBuilder.append("\n") + } + return markdownBuilder.toString().trim() +} + +fun Note.getExportableSplitNote(): ExportableSplitNote { + return ExportableSplitNote( + toExportedMarkdown(), + getExportableNoteMeta()) +} fun Note.getExportableNoteMeta(): ExportableNoteMeta { return ExportableNoteMeta( @@ -18,6 +70,19 @@ fun Note.getExportableNoteMeta(): ExportableNoteMeta { ) } +fun Note.mergeMetas(meta: ExportableNoteMeta) { + uuid = meta.uuid + state = meta.state + timestamp = meta.timestamp + updateTimestamp = meta.updateTimestamp + color = meta.color + tags = meta.tags + pinned = meta.pinned + locked = meta.locked + folder = meta.folder +} + + fun Folder.getExportableFolder(): ExportableFolder { return ExportableFolder( uuid, diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt index f944b80a..003f75d8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt @@ -9,13 +9,9 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb -import com.maubis.scarlet.base.export.data.ExportableFileFormat -import com.maubis.scarlet.base.export.data.ExportableFolder -import com.maubis.scarlet.base.export.data.ExportableNote -import com.maubis.scarlet.base.export.data.ExportableTag +import com.maubis.scarlet.base.export.data.* import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FILENAME import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FOLDER -import com.maubis.scarlet.base.note.getFullText import java.io.File import java.util.* @@ -64,7 +60,7 @@ class NoteExporter() { private fun getMarkdownExportContent(): String { var totalText = "$EXPORT_NOTE_SEPARATOR\n\n" notesDb.getAll() - .map { it.getFullText() } + .map { it.toExportedMarkdown() } .forEach { totalText += it totalText += "\n\n$EXPORT_NOTE_SEPARATOR\n\n" diff --git a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt index 5789689b..1f2cf151 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.note import com.maubis.markdown.segmenter.MarkdownSegment import com.maubis.markdown.segmenter.MarkdownSegmentType +import com.maubis.markdown.segmenter.TextSegmenter import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType @@ -29,4 +30,61 @@ fun MarkdownSegmentType.toFormatType(): FormatType { MarkdownSegmentType.CHECKLIST_UNCHECKED -> FormatType.CHECKLIST_UNCHECKED MarkdownSegmentType.CHECKLIST_CHECKED -> FormatType.CHECKLIST_CHECKED } +} + +fun String.toInternalFormats(): List { + return toInternalFormats(arrayOf( + MarkdownSegmentType.HEADING_1, + MarkdownSegmentType.HEADING_2, + MarkdownSegmentType.CODE, + MarkdownSegmentType.QUOTE, + MarkdownSegmentType.CHECKLIST_UNCHECKED, + MarkdownSegmentType.CHECKLIST_CHECKED, + MarkdownSegmentType.SEPARATOR)) +} + +/** + * Converts a string to the internal format types using the Markdown Segmentation Library. + * It's possible to pass specific formats which will be preserved in the formats + */ +fun String.toInternalFormats(whitelistedSegments: Array): List { + val extractedFormats = emptyList().toMutableList() + val segments = TextSegmenter(this).get() + + var lastFormat: Format? = null + segments.forEach { segment -> + val isSegmentWhitelisted = whitelistedSegments.contains(segment.type()) + val newFormat = when { + !isSegmentWhitelisted -> null + segment.type() == MarkdownSegmentType.HEADING_1 -> Format(FormatType.HEADING, segment.strip()) + segment.type() == MarkdownSegmentType.HEADING_2 -> Format(FormatType.SUB_HEADING, segment.strip()) + segment.type() == MarkdownSegmentType.CODE -> Format(FormatType.CODE, segment.strip()) + segment.type() == MarkdownSegmentType.QUOTE -> Format(FormatType.QUOTE, segment.strip()) + segment.type() == MarkdownSegmentType.CHECKLIST_UNCHECKED -> Format(FormatType.CHECKLIST_UNCHECKED, segment.strip()) + segment.type() == MarkdownSegmentType.CHECKLIST_CHECKED -> Format(FormatType.CHECKLIST_CHECKED, segment.strip()) + segment.type() == MarkdownSegmentType.SEPARATOR -> Format(FormatType.SEPARATOR) + else -> null + } + + val tempLastFormat = lastFormat + when { + tempLastFormat !== null && newFormat !== null -> { + extractedFormats.add(tempLastFormat) + extractedFormats.add(newFormat) + lastFormat = null + } + tempLastFormat === null && newFormat !== null -> { + extractedFormats.add(newFormat) + } + tempLastFormat !== null && newFormat === null -> { + tempLastFormat.text += "\n" + tempLastFormat.text += segment.text() + lastFormat = tempLastFormat + } + tempLastFormat == null && newFormat === null -> { + lastFormat = Format(FormatType.TEXT, segment.text()) + } + } + } + return extractedFormats } \ No newline at end of file diff --git a/markdown/src/main/java/com/maubis/markdown/Markdown.kt b/markdown/src/main/java/com/maubis/markdown/Markdown.kt index 1dd969a0..f8bcd05c 100644 --- a/markdown/src/main/java/com/maubis/markdown/Markdown.kt +++ b/markdown/src/main/java/com/maubis/markdown/Markdown.kt @@ -28,12 +28,6 @@ object Markdown { return spannable } - fun toMarkwonableText(text: String): String { - val source = TextInliner(text).get().toMarkwon() - source.replace(Regex("(\\S)\n(\\S)"), "$1 \n$2") - return source - } - fun getSpanInfo(text: String, stripDelimiter: Boolean = false): SpanResult { val segments = TextSegmenter(text).get() var currentIndex = 0 diff --git a/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt b/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt index 5f1519bd..e5189f1a 100644 --- a/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt +++ b/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt @@ -19,10 +19,19 @@ class MarkdownSegmentBuilder { abstract class MarkdownSegment { + /** + * The type of the segment + */ abstract fun type(): MarkdownSegmentType + /** + * Strip the segment separators and return the text inside the segment which is formatted + */ abstract fun strip(): String + /** + * Return the entire text which the segment contains including the delimiters + */ abstract fun text(): String } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 5d6714d5..18fa7dd6 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -6,16 +6,20 @@ import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.bijoysingh.quicknote.database.genGDriveUploadDatabase -import com.bijoysingh.quicknote.firebase.data.FirebaseNote import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.format.FormatType -import com.maubis.scarlet.base.core.note.INoteContainer +import com.maubis.scarlet.base.core.note.NoteBuilder +import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.data.* +import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference @@ -74,7 +78,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var isValidController: Boolean = true private var driveHelper: GDriveServiceHelper? = null - private var notesSync: GDriveRemoteFolder? = null + private var notesSync: GDriveRemoteFolder? = null + private var notesMetaSync: GDriveRemoteFolder? = null private var foldersSync: GDriveRemoteFolder? = null private var tagsSync: GDriveRemoteFolder? = null private var imageSync: GDriveRemoteImageFolder? = null @@ -89,16 +94,39 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { driveHelper = helper gDriveDatabase = genGDriveUploadDatabase(context) - notesSync = GDriveRemoteFolder(GDriveDataType.NOTE, gDriveDatabase!!, helper) { - ApplicationBase.instance.notesDatabase().getByUUID(it)?.getFirebaseNote() - } - tagsSync = GDriveRemoteFolder(GDriveDataType.TAG, gDriveDatabase!!, helper) { - ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() - } - foldersSync = GDriveRemoteFolder(GDriveDataType.FOLDER, gDriveDatabase!!, helper) { - ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() - } - imageSync = GDriveRemoteImageFolder(GDriveDataType.IMAGE, gDriveDatabase!!, helper) + notesSync = GDriveRemoteFolder( + dataType = GDriveDataType.NOTE, + database = gDriveDatabase!!, + helper = helper, + serialiser = { it }, + uuidToObject = { + ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() + }) + notesMetaSync = GDriveRemoteFolder( + dataType = GDriveDataType.NOTE_META, + database = gDriveDatabase!!, + helper = helper, + serialiser = { Gson().toJson(it) }, + uuidToObject = { + ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() + }) + tagsSync = GDriveRemoteFolder( + dataType = GDriveDataType.TAG, + database = gDriveDatabase!!, + helper = helper, + serialiser = { Gson().toJson(it) }, + uuidToObject = { + ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() + }) + foldersSync = GDriveRemoteFolder( + dataType = GDriveDataType.FOLDER, + database = gDriveDatabase!!, + helper = helper, + serialiser = { Gson().toJson(it) }, + uuidToObject = { + ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() + }) + imageSync = GDriveRemoteImageFolder(dataType = GDriveDataType.IMAGE, database = gDriveDatabase!!, helper = helper) GlobalScope.launch { val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) @@ -224,15 +252,16 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } when { - data is ExportableTag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid) - data is ExportableFolder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) - data is FirebaseNote -> notifyInsertImpl(data) - data is ExportableNoteMeta -> localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) + data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid) + data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + data is Note -> notifyInsertImpl(data) } } - fun notifyInsertImpl(note: FirebaseNote) { - localDatabaseUpdate(GDriveDataType.NOTE, note.uuid) + fun notifyInsertImpl(note: Note) { + val noteUuid = note.uuid + localDatabaseUpdate(GDriveDataType.NOTE, noteUuid) + localDatabaseUpdate(GDriveDataType.NOTE_META, noteUuid) val database = gDriveDatabase if (database === null) { @@ -278,10 +307,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } when { - data is ExportableTag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, true) - data is ExportableFolder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, true) - data is FirebaseNote -> localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, true) - data is ExportableNoteMeta -> localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, true) + data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, true) + data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, true) + data is Note -> { + localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, true) + localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, true) + } } } @@ -378,11 +409,15 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { when (type) { GDriveDataType.NOTE -> { - ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getFirebaseNote()?.apply { - notesSync?.insert(this.uuid, this) + ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { + notesSync?.insert(data.uuid, this) + } + } + GDriveDataType.NOTE_META -> { + ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { + notesMetaSync?.insert(data.uuid, this) } } - GDriveDataType.NOTE_META -> TODO() GDriveDataType.TAG -> { ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { tagsSync?.insert(this.uuid, this) @@ -409,7 +444,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { val uuid = data.uuid when (type) { GDriveDataType.NOTE -> notesSync?.delete(uuid) - GDriveDataType.NOTE_META -> TODO() + GDriveDataType.NOTE_META -> notesMetaSync?.delete(uuid) GDriveDataType.TAG -> tagsSync?.delete(uuid) GDriveDataType.FOLDER -> foldersSync?.delete(uuid) GDriveDataType.IMAGE -> { @@ -434,15 +469,37 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { when (type) { GDriveDataType.NOTE -> { onRemoteInsertImpl(data.fileId) { + // TODO: De-duplicate meta data and note update try { - val item = Gson().fromJson(it, FirebaseNote::class.java) - IRemoteDatabaseUtils.onRemoteInsert(context, item) - remoteDatabaseUpdate(GDriveDataType.NOTE, item.uuid) + val itemDescription = fromExportedMarkdown(it) + val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) + ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } + val temporaryNote = NoteBuilder().copy(existingNote) + temporaryNote.description = itemDescription + IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) + + remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) + } catch (exception: Exception) { + } + } + } + GDriveDataType.NOTE_META -> { + // TODO: De-duplicate meta data and note update + onRemoteInsertImpl(data.fileId) { + try { + val item = Gson().fromJson(it, ExportableNoteMeta::class.java) + + val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) + ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } + val temporaryNote = NoteBuilder().copy(existingNote) + temporaryNote.mergeMetas(item) + IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) + + remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) } catch (exception: Exception) { } } } - GDriveDataType.NOTE_META -> TODO() GDriveDataType.TAG -> { onRemoteInsertImpl(data.fileId) { try { @@ -491,8 +548,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } when (type) { - GDriveDataType.NOTE -> IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) - GDriveDataType.NOTE_META -> TODO() + GDriveDataType.NOTE -> { + IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) + remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) + } + GDriveDataType.NOTE_META -> { + } // Should never happen as note is handling this deletion GDriveDataType.TAG -> IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) GDriveDataType.FOLDER -> IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) GDriveDataType.IMAGE -> { @@ -519,14 +580,13 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } - private fun notifyImageIds(note: INoteContainer, onImageUUID: (ImageUUID) -> Unit) { - val imageIds = FormatBuilder() - .getFormats(note.description()) + private fun notifyImageIds(note: Note, onImageUUID: (ImageUUID) -> Unit) { + val imageIds = note.getFormats() .filter { it.formatType == FormatType.IMAGE } .map { it.text } .toSet() imageIds.forEach { - onImageUUID(ImageUUID(note.uuid(), it)) + onImageUUID(ImageUUID(note.uuid, it)) } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index d8eceadc..c8334acd 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -2,7 +2,6 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadDataDao -import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -12,7 +11,8 @@ class GDriveRemoteFolder( dataType: GDriveDataType, database: GDriveUploadDataDao, helper: GDriveServiceHelper, - val uuidToObject: (String) -> T?): GDriveRemoteFolderBase(dataType, database, helper) { + val serialiser: (T) -> String, + val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper) { var contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -83,6 +83,9 @@ class GDriveRemoteFolder( } } + /** + * Insert the file on the server based on the insertion on the local device + */ fun insert(uuid: String, item: T) { if (contentLoading.get()) { contentPendingActions.add(uuid) @@ -90,9 +93,10 @@ class GDriveRemoteFolder( } try { - val data = Gson().toJson(item) + val data = serialiser(item) val fileId = contentFiles[uuid] - val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp + ?: getTrueCurrentTime() if (fileId !== null) { helper.saveFile(fileId, uuid, data, timestamp).addOnCompleteListener { val file = it.result @@ -113,6 +117,9 @@ class GDriveRemoteFolder( } } + /** + * Delete the file on the server based on removal on the local device + */ fun delete(uuid: String) { if (deletedLoading.get() || contentLoading.get()) { deletedPendingActions.add(uuid) @@ -125,7 +132,8 @@ class GDriveRemoteFolder( contentFiles.remove(uuid) } - val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp + ?: getTrueCurrentTime() helper.createFileWithData(deletedFolderUid, uuid, "", timestamp).addOnCompleteListener { val file = it.result if (file !== null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 781510bf..734a1055 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -1,7 +1,6 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.firebase.data.getFirebaseFolder import com.maubis.scarlet.base.core.folder.MaterialFolderActor import com.maubis.scarlet.base.database.room.folder.Folder @@ -9,15 +8,11 @@ class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { override fun onlineSave() { super.onlineSave() - // TODO: Remove this completely, Not doing this anymore. - // firebase?.insert(folder.getFirebaseFolder()) - gDrive?.notifyInsert(folder.getFirebaseFolder()) + gDrive?.notifyInsert(folder) } override fun delete() { super.delete() - // TODO: Remove this completely, Not doing this anymore. - // firebase?.remove(folder.getFirebaseFolder()) - gDrive?.notifyRemove(folder.getFirebaseFolder()) + gDrive?.notifyRemove(folder) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index 334330a6..b59a9186 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -11,15 +11,11 @@ class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { override fun onlineSave(context: Context) { super.onlineSave(context) - // TODO: Remove this completely, Not doing this anymore. - // firebase?.insert(note.getFirebaseNote()) - gDrive?.notifyInsert(note.getFirebaseNote()) + gDrive?.notifyInsert(note) } override fun onlineDelete(context: Context) { super.onlineDelete(context) - // TODO: Remove this completely, Not doing this anymore. - // firebase?.remove(note.getFirebaseNote()) - gDrive?.notifyRemove(note.getFirebaseNote()) + gDrive?.notifyRemove(note) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 5c67876e..874a704c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -1,8 +1,6 @@ package com.bijoysingh.quicknote.scarlet -import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.firebase.data.getFirebaseTag import com.maubis.scarlet.base.core.tag.MaterialTagActor import com.maubis.scarlet.base.database.room.tag.Tag @@ -10,15 +8,11 @@ class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { override fun onlineSave() { super.onlineSave() - // TODO: Remove this completely, Not doing this anymore. - // firebase?.insert(tag.getFirebaseTag()) - gDrive?.notifyInsert(tag.getFirebaseTag()) + gDrive?.notifyInsert(tag) } override fun delete() { super.delete() - // TODO: Remove this completely, Not doing this anymore. - // firebase?.remove(tag.getFirebaseTag()) - gDrive?.notifyRemove(tag.getFirebaseTag()) + gDrive?.notifyRemove(tag) } } \ No newline at end of file From 3ec4c7ad1f81e36dc431c22496c35abb5cb85a71 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 2 Jun 2019 00:26:45 +0100 Subject: [PATCH 031/134] [GDrive] Fixing upload and downloads with new note format --- .../scarlet/base/core/note/NoteExtensions.kt | 3 +- .../base/export/data/ExportableExtensions.kt | 5 +-- .../scarlet/base/note/MarkdownExtensions.kt | 36 +++++-------------- .../scarlet/base/note/NoteExtensions.kt | 11 ++++-- .../base/widget/AllNotesWidgetService.kt | 2 +- base/src/main/res/values/strings.xml | 2 +- .../markdown/segmenter/MarkdownSegment.kt | 2 +- .../markdown/segmenter/MarkdownSegmentType.kt | 1 + .../markdown/segmenter/TextSegmentConfig.kt | 27 +++++++------- .../com/maubis/markdown/spannable/SpanInfo.kt | 2 ++ .../markdown/spannable/SpannableExtensions.kt | 1 + .../quicknote/drive/GDriveLoginActivity.kt | 25 +++---------- .../quicknote/drive/GDriveLogoutActivity.kt | 1 - .../quicknote/drive/GDriveRemoteDatabase.kt | 7 ++-- .../quicknote/drive/GDriveRemoteFolder.kt | 3 ++ .../drive/GDriveRemoteImageFolder.kt | 22 ------------ .../quicknote/drive/GDriveServiceHelper.kt | 28 +++++++-------- 17 files changed, 65 insertions(+), 113 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt index 2748f5e8..0474728a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt @@ -62,6 +62,5 @@ fun Note.setReminderV2(reminder: Reminder) { fun Note.getTagUUIDs(): MutableSet { val tags = if (this.tags == null) "" else this.tags - val split = tags.split(",") - return HashSet(split) + return tags.split(",").filter { it.isNotBlank() }.toMutableSet() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt index fa77de70..6964ed4a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt @@ -1,8 +1,5 @@ package com.maubis.scarlet.base.export.data -import com.maubis.markdown.segmenter.MarkdownSegmentType -import com.maubis.markdown.segmenter.TextSegmenter -import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.getFormats @@ -35,7 +32,7 @@ fun Note.toExportedMarkdown(): String { FormatType.CODE -> "```\n$text\n```" FormatType.QUOTE -> "> $text" // TODO: Fix the fact that markdown parsing wont parse this correctly - FormatType.IMAGE -> "$text" + FormatType.IMAGE -> "$text" FormatType.SEPARATOR -> "\n---\n" FormatType.TEXT -> text diff --git a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt index 1f2cf151..c104e51b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt @@ -1,37 +1,10 @@ package com.maubis.scarlet.base.note -import com.maubis.markdown.segmenter.MarkdownSegment import com.maubis.markdown.segmenter.MarkdownSegmentType import com.maubis.markdown.segmenter.TextSegmenter import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType -fun MarkdownSegment.toFormat(): Format { - return Format(type().toFormatType(), strip()) -} - -fun MarkdownSegment.toRawFormat(): Format { - return Format(type().toFormatType(), text()) -} - -fun MarkdownSegmentType.toFormatType(): FormatType { - return when (this) { - MarkdownSegmentType.INVALID -> FormatType.EMPTY - MarkdownSegmentType.HEADING_1 -> FormatType.HEADING - MarkdownSegmentType.HEADING_2 -> FormatType.SUB_HEADING - MarkdownSegmentType.HEADING_3 -> FormatType.HEADING_3 - MarkdownSegmentType.NORMAL -> FormatType.TEXT - MarkdownSegmentType.CODE -> FormatType.CODE - MarkdownSegmentType.BULLET_1 -> FormatType.TEXT - MarkdownSegmentType.BULLET_2 -> FormatType.TEXT - MarkdownSegmentType.BULLET_3 -> FormatType.TEXT - MarkdownSegmentType.QUOTE -> FormatType.QUOTE - MarkdownSegmentType.SEPARATOR -> FormatType.SEPARATOR - MarkdownSegmentType.CHECKLIST_UNCHECKED -> FormatType.CHECKLIST_UNCHECKED - MarkdownSegmentType.CHECKLIST_CHECKED -> FormatType.CHECKLIST_CHECKED - } -} - fun String.toInternalFormats(): List { return toInternalFormats(arrayOf( MarkdownSegmentType.HEADING_1, @@ -40,7 +13,8 @@ fun String.toInternalFormats(): List { MarkdownSegmentType.QUOTE, MarkdownSegmentType.CHECKLIST_UNCHECKED, MarkdownSegmentType.CHECKLIST_CHECKED, - MarkdownSegmentType.SEPARATOR)) + MarkdownSegmentType.SEPARATOR, + MarkdownSegmentType.IMAGE)) } /** @@ -63,6 +37,7 @@ fun String.toInternalFormats(whitelistedSegments: Array): L segment.type() == MarkdownSegmentType.CHECKLIST_UNCHECKED -> Format(FormatType.CHECKLIST_UNCHECKED, segment.strip()) segment.type() == MarkdownSegmentType.CHECKLIST_CHECKED -> Format(FormatType.CHECKLIST_CHECKED, segment.strip()) segment.type() == MarkdownSegmentType.SEPARATOR -> Format(FormatType.SEPARATOR) + segment.type() == MarkdownSegmentType.IMAGE -> Format(FormatType.IMAGE, segment.strip().trim()) else -> null } @@ -86,5 +61,10 @@ fun String.toInternalFormats(whitelistedSegments: Array): L } } } + + val tempLastFormat = lastFormat + if (tempLastFormat !== null) { + extractedFormats.add(tempLastFormat) + } return extractedFormats } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index cbe83509..77759ac3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -4,8 +4,8 @@ import android.content.Context import android.content.Intent import com.github.bijoysingh.starter.util.DateFormatter import com.google.gson.Gson +import com.maubis.markdown.BuildConfig import com.maubis.markdown.Markdown -import com.maubis.markdown.segmenter.TextSegmenter import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.format.Format @@ -97,7 +97,7 @@ fun Note.getSmartFormats(): List { val smartFormats = ArrayList() formats.forEach { if (it.formatType == FormatType.TEXT) { - val moreFormats = TextSegmenter(it.text).get().map { it.toFormat() } + val moreFormats = it.text.toInternalFormats() moreFormats.forEach { format -> format.uid = maxIndex smartFormats.add(format) @@ -154,10 +154,15 @@ fun Note.getUnreliablyStrippedText(context: Context): String { } fun Note.getLockedText(isMarkdownEnabled: Boolean): CharSequence { - return when { + val text = when { this.locked -> "******************\n***********\n****************" else -> getMarkdownText(isMarkdownEnabled) } + + if (BuildConfig.DEBUG) { + return "`$uuid`\n$text" + } + return text; } fun Note.getDisplayTime(): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt index 530340b8..cb3b8faf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt @@ -46,7 +46,7 @@ class AllNotesRemoteViewsFactory(val context: Context) : RemoteViewsService.Remo } override fun getViewAt(position: Int): RemoteViews? { - if (position == AdapterView.INVALID_POSITION) { + if (position == AdapterView.INVALID_POSITION || position >= notes.size) { return null } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 6eb79f56..19935df7 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -443,7 +443,7 @@ Sign Out of Scarlet Scarlet will stop syncing your notes, tags and folders to your Google Drive. Sign Out from Drive - Signing Out In… + Signing Out… Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. Your photos are uploaded and synced across devices as well. diff --git a/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt b/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt index e5189f1a..bc99011f 100644 --- a/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt +++ b/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegment.kt @@ -49,7 +49,7 @@ class LineStartMarkdownSegment(val config: LineStartSegment, val text: String) : override fun type() = config.type() override fun strip(): String { - return "${config.replacementToken}${text.removePrefix(config.lineStartToken)}" + return text.removePrefix(config.lineStartToken) } override fun text(): String = text diff --git a/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegmentType.kt b/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegmentType.kt index 6887ee55..c3c350ec 100644 --- a/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegmentType.kt +++ b/markdown/src/main/java/com/maubis/markdown/segmenter/MarkdownSegmentType.kt @@ -6,6 +6,7 @@ enum class MarkdownSegmentType { HEADING_2, HEADING_3, NORMAL, + IMAGE, CODE, BULLET_1, BULLET_2, diff --git a/markdown/src/main/java/com/maubis/markdown/segmenter/TextSegmentConfig.kt b/markdown/src/main/java/com/maubis/markdown/segmenter/TextSegmentConfig.kt index bc01c513..2929084d 100644 --- a/markdown/src/main/java/com/maubis/markdown/segmenter/TextSegmentConfig.kt +++ b/markdown/src/main/java/com/maubis/markdown/segmenter/TextSegmentConfig.kt @@ -20,8 +20,7 @@ class FullLineSegment(val type: MarkdownSegmentType, val lineToken: String) : IS } class LineStartSegment(val type: MarkdownSegmentType, - val lineStartToken: String, - val replacementToken: String = "") : ISegmentConfig { + val lineStartToken: String) : ISegmentConfig { override fun type() = type override fun isValid(segment: String): Boolean { @@ -116,14 +115,14 @@ class TextSegmentConfig(builder: Builder) { MarkdownSegmentType.CODE -> arrayOf( MultilineDelimiterSegment(MarkdownSegmentType.CODE, "```", "```")) MarkdownSegmentType.BULLET_1 -> arrayOf( - LineStartSegment(MarkdownSegmentType.BULLET_1, "- ", "• ")) + LineStartSegment(MarkdownSegmentType.BULLET_1, "- ")) MarkdownSegmentType.BULLET_2 -> arrayOf( - LineStartSegment(MarkdownSegmentType.BULLET_2, " - ", " ◦ "), - LineStartSegment(MarkdownSegmentType.BULLET_2, " - ", " ◦ ")) + LineStartSegment(MarkdownSegmentType.BULLET_2, " - "), + LineStartSegment(MarkdownSegmentType.BULLET_2, " - ")) MarkdownSegmentType.BULLET_3 -> arrayOf( - LineStartSegment(MarkdownSegmentType.BULLET_3, " - ", " ▪ "), - LineStartSegment(MarkdownSegmentType.BULLET_3, " - ", " ▪ "), - LineStartSegment(MarkdownSegmentType.BULLET_3, " - ", " ▪ ")) + LineStartSegment(MarkdownSegmentType.BULLET_3, " - "), + LineStartSegment(MarkdownSegmentType.BULLET_3, " - "), + LineStartSegment(MarkdownSegmentType.BULLET_3, " - ")) MarkdownSegmentType.QUOTE -> arrayOf( MultilineStartSegment(MarkdownSegmentType.QUOTE, "> ")) MarkdownSegmentType.SEPARATOR -> arrayOf( @@ -134,11 +133,14 @@ class TextSegmentConfig(builder: Builder) { FullLineSegment(MarkdownSegmentType.SEPARATOR, "------"), FullLineSegment(MarkdownSegmentType.SEPARATOR, "-------")) MarkdownSegmentType.CHECKLIST_UNCHECKED -> arrayOf( - LineStartSegment(MarkdownSegmentType.CHECKLIST_UNCHECKED, "[] ", "☐ "), - LineStartSegment(MarkdownSegmentType.CHECKLIST_UNCHECKED, "[ ] ", "☐ ")) + LineStartSegment(MarkdownSegmentType.CHECKLIST_UNCHECKED, "[] "), + LineStartSegment(MarkdownSegmentType.CHECKLIST_UNCHECKED, "[ ] ")) MarkdownSegmentType.CHECKLIST_CHECKED -> arrayOf( - LineStartSegment(MarkdownSegmentType.CHECKLIST_CHECKED, "[x] ", "☑ "), - LineStartSegment(MarkdownSegmentType.CHECKLIST_CHECKED, "[X] ", "☑ ")) + LineStartSegment(MarkdownSegmentType.CHECKLIST_CHECKED, "[x] "), + LineStartSegment(MarkdownSegmentType.CHECKLIST_CHECKED, "[X] ")) + MarkdownSegmentType.IMAGE -> arrayOf( + LineDelimiterSegment(MarkdownSegmentType.IMAGE, "<"), + LineDelimiterSegment(MarkdownSegmentType.IMAGE, "", "")) } } @@ -157,6 +159,7 @@ class TextSegmentConfig(builder: Builder) { MarkdownSegmentType.SEPARATOR -> LineStartSegment(MarkdownSegmentType.SEPARATOR, "---") MarkdownSegmentType.CHECKLIST_UNCHECKED -> LineStartSegment(MarkdownSegmentType.CHECKLIST_UNCHECKED, "[ ] ") MarkdownSegmentType.CHECKLIST_CHECKED -> LineStartSegment(MarkdownSegmentType.CHECKLIST_CHECKED, "[x] ") + MarkdownSegmentType.IMAGE -> LineDelimiterSegment(MarkdownSegmentType.IMAGE, "") } } } diff --git a/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt b/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt index 0d9525ee..fc12af81 100644 --- a/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt +++ b/markdown/src/main/java/com/maubis/markdown/spannable/SpanInfo.kt @@ -22,6 +22,7 @@ enum class MarkdownType { SEPARATOR, CHECKLIST_UNCHECKED, CHECKLIST_CHECKED, + IMAGE, } data class SpanResult(val text: String, val spans: List) @@ -43,6 +44,7 @@ fun map(type: MarkdownSegmentType): MarkdownType { MarkdownSegmentType.SEPARATOR -> MarkdownType.SEPARATOR MarkdownSegmentType.CHECKLIST_UNCHECKED -> MarkdownType.CHECKLIST_UNCHECKED MarkdownSegmentType.CHECKLIST_CHECKED -> MarkdownType.CHECKLIST_CHECKED + MarkdownSegmentType.IMAGE -> MarkdownType.IMAGE } } diff --git a/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt b/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt index 41297edd..c1c0c24e 100644 --- a/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt +++ b/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt @@ -113,6 +113,7 @@ fun Spannable.setFormats(info: SpanInfo) { MarkdownType.INLINE_CODE -> inlineCode(s, e) MarkdownType.STRIKE -> strike(s, e) MarkdownType.SEPARATOR -> separator(s, e) + MarkdownType.IMAGE -> monospace(s, e).code(s, e) else -> { } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 57bfe5bd..136e3654 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -27,6 +27,7 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -142,31 +143,13 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed return@launch } ApplicationBase.instance.notesDatabase().getAll().forEach { - val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.NOTE.name, it.uuid) - ?: GDriveUploadData() - existing.apply { - uuid = it.uuid - type = GDriveDataType.NOTE.name - save(database) - } + instance.noteActions(it).onlineSave(context) } ApplicationBase.instance.tagsDatabase().getAll().forEach { - val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.TAG.name, it.uuid) - ?: GDriveUploadData() - existing.apply { - uuid = it.uuid - type = GDriveDataType.TAG.name - save(database) - } + instance.tagActions(it).onlineSave() } ApplicationBase.instance.foldersDatabase().getAll().forEach { - val existing = gDrive?.gDriveDatabase?.getByUUID(GDriveDataType.FOLDER.name, it.uuid) - ?: GDriveUploadData() - existing.apply { - uuid = it.uuid - type = GDriveDataType.FOLDER.name - save(database) - } + instance.folderActions(it).onlineSave() } finish() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt index e870294c..b828841d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt @@ -86,7 +86,6 @@ class GDriveLogoutActivity : ThemedActivity(), GoogleApiClient.OnConnectionFaile fun onSignOutComplete() { instance.authenticator().logout() - setButton(false) finish() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 18fa7dd6..99655775 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -159,6 +159,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { fun logout() { GlobalScope.launch { reset() + gDriveDatabase?.drop() gDriveConfig?.clearSync() } } @@ -174,7 +175,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { sGDriveFirstSyncNote = true } } - FOLDER_NAME_NOTES_META -> notesSync?.initContentFolderId(folderId) { + FOLDER_NAME_NOTES_META -> notesMetaSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNoteMeta) { GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } sGDriveFirstSyncNoteMeta = true @@ -530,7 +531,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { - remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + if (it.result == true) { + remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + } } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index c8334acd..ef731775 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -132,6 +132,9 @@ class GDriveRemoteFolder( contentFiles.remove(uuid) } + if (deletedFolderUid == INVALID_FILE_ID) { + return + } val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() helper.createFileWithData(deletedFolderUid, uuid, "", timestamp).addOnCompleteListener { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 1ac1afb6..5dd35e1d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -69,28 +69,6 @@ class GDriveRemoteImageFolder( } } - fun onRemoteInsert(id: ImageUUID) { - if (contentLoading.get()) { - contentPendingActions.add(id) - return - } - - if (contentFiles.containsKey(id)) { - return - } - - val gDriveUUID = id.name() - val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() - val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) - helper.createFileWithData(contentFolderUid, gDriveUUID, imageFile, timestamp).addOnCompleteListener { - val file = it.result - if (file !== null) { - contentFiles[id] = file.id - notifyDriveData(file.id, gDriveUUID, timestamp) - } - } - } - fun insert(id: ImageUUID) { if (contentLoading.get()) { contentPendingActions.add(id) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 76b82c4f..38b04b69 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -27,7 +27,7 @@ const val GOOGLE_DRIVE_FILE_MIME_TYPE = "text/plain" const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" -const val MAX_THRESHOLD_QUERIES_PER_SECOND = 5 +const val MAX_THRESHOLD_QUERIES_PER_SECOND = 2 const val MIN_RESET_QUERIES_PER_SECOND = 0.1 var lastCheckpointTime: AtomicLong = AtomicLong(0L) @@ -81,7 +81,7 @@ fun getTrueCurrentTime(): Long { } class GDriveServiceHelper(private val mDriveService: Drive) { - private val mExecutor = Executors.newFixedThreadPool(8) + private val mExecutor = Executors.newFixedThreadPool(2) fun execute(action: String = "", callable: Callable): Task { return Tasks.call(mExecutor, ErrorCallable(action, callable)) @@ -133,26 +133,24 @@ class GDriveServiceHelper(private val mDriveService: Drive) { return execute("readFile", Callable { mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> BufferedReader(InputStreamReader(`is`)).use { reader -> - val stringBuilder = StringBuilder() - var line: String? = reader.readLine() - while (line !== null) { - stringBuilder.append(line) - line = reader.readLine() - } - val contents = stringBuilder.toString() - contents + reader.readText() } } }) } - fun readFile(fileId: String, destinationFile: java.io.File): Task { + fun readFile(fileId: String, destinationFile: java.io.File): Task { log("GDrive", "readFile($fileId, ${destinationFile.absolutePath})") - return execute("readFile", Callable { + return execute("readFile", Callable { destinationFile.parentFile.mkdirs() - val fileStream = FileOutputStream(destinationFile) - mDriveService.files().get(fileId).executeMediaAndDownloadTo(fileStream) - null + try { + val fileStream = FileOutputStream(destinationFile) + mDriveService.files().get(fileId).executeMediaAndDownloadTo(fileStream) + fileStream.close() + } catch (exception: Exception) { + return@Callable false + } + destinationFile.exists() }) } From dbb0e4c3906cc519997822725171cd6cf55b840e Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 2 Jun 2019 14:52:07 +0100 Subject: [PATCH 032/134] [GDrive] Making Sync better --- .../com/maubis/scarlet/base/MainActivity.kt | 46 ++++++++-- .../maubis/scarlet/base/config/CoreConfig.kt | 2 +- .../scarlet/base/config/MaterialNoteConfig.kt | 2 +- .../base/config/auth/IAuthenticator.kt | 2 + .../base/config/auth/NullAuthenticator.kt | 1 + .../main/specs/MainActivityBottomBarSpec.kt | 40 +++++++++ base/src/main/res/layout/activity_main.xml | 33 ------- .../2.json | 85 +++++++++++++++++++ .../quicknote/database/GDriveUploadData.kt | 10 ++- .../database/GDriveUploadDatabase.kt | 6 +- .../quicknote/drive/GDriveAuthenticator.kt | 2 + .../quicknote/drive/GDriveRemoteDatabase.kt | 44 ++++++++-- .../quicknote/drive/GDriveRemoteFolderBase.kt | 25 +++++- .../quicknote/scarlet/ScarletAuthenticator.kt | 8 +- .../quicknote/scarlet/ScarletConfig.kt | 4 +- 15 files changed, 253 insertions(+), 57 deletions(-) create mode 100644 scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/2.json diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 59c63411..9eaef989 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -27,6 +27,7 @@ import com.maubis.scarlet.base.main.sheets.openDeleteTrashSheet import com.maubis.scarlet.base.main.specs.MainActivityBottomBar import com.maubis.scarlet.base.main.specs.MainActivityDisabledSync import com.maubis.scarlet.base.main.specs.MainActivityFolderBottomBar +import com.maubis.scarlet.base.main.specs.MainActivitySyncingNow import com.maubis.scarlet.base.main.utils.MainSnackbar import com.maubis.scarlet.base.note.activity.INoteOptionSheetActivity import com.maubis.scarlet.base.note.folder.FolderRecyclerItem @@ -59,6 +60,7 @@ import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.search_toolbar_main.* import kotlinx.android.synthetic.main.toolbar_trash_info.* import kotlinx.coroutines.* +import java.lang.ref.WeakReference class MainActivity : ThemedActivity(), INoteOptionSheetActivity { private val singleThreadDispatcher = newSingleThreadContext("singleThreadDispatcher") @@ -322,6 +324,41 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { .build())) } + fun notifySyncingInformation() { + val componentContext = ComponentContext(this) + if (!instance.authenticator().isLoggedIn(this) + || instance.authenticator().isLegacyLoggedIn()) { + return + } + + GlobalScope.launch { + if (!instance.authenticator().isDataPendingUpload()) { + GlobalScope.launch(Dispatchers.Main) { + lithoPreBottomToolbar.removeAllViews() + } + return@launch + } + + GlobalScope.launch(Dispatchers.Main) { + lithoPreBottomToolbar.removeAllViews() + lithoPreBottomToolbar.addView(LithoView.create(componentContext, + MainActivitySyncingNow.create(componentContext) + .onClick {} + .build())) + + val weakActivity = WeakReference(this@MainActivity) + ApplicationBase.instance.resyncDrive { + GlobalScope.launch(Dispatchers.Main) { + val instance = weakActivity.get() + if (instance !== null && !instance.isDestroyed) { + instance.notifySyncingInformation() + } + } + } + } + } + } + fun unifiedSearch() { GlobalScope.launch(Dispatchers.Main) { val items = GlobalScope.async(Dispatchers.IO) { unifiedSearchSynchronous() } @@ -342,15 +379,8 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { setupData() registerNoteReceiver() - topSyncingLayout.visibility = View.VISIBLE - GlobalScope.launch { - ApplicationBase.instance.resyncDrive(false) { - GlobalScope.launch(Dispatchers.Main) { - topSyncingLayout.visibility = GONE - } - } - } notifyDisabledSync() + notifySyncingInformation() } fun resetAndSetupData() { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 3c6d1b7e..66d253cf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -65,7 +65,7 @@ abstract class CoreConfig(context: Context) { abstract fun imageCache(): ImageCache - abstract fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) + abstract fun resyncDrive(onSyncCompleted: () -> Unit) companion object { val notesDb get() = instance.notesDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 1208ad8d..95e28d21 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -67,5 +67,5 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun imageCache(): ImageCache = imageCache - override fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) = onSyncCompleted() + override fun resyncDrive(onSyncCompleted: () -> Unit) = onSyncCompleted() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index 5b225c88..2a9357b3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -10,6 +10,8 @@ interface IAuthenticator { fun isLegacyLoggedIn(): Boolean + fun isDataPendingUpload(): Boolean + fun userId(context: Context): String? fun openLoginActivity(context: Context): Runnable? diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index 3ec3a330..8ef72026 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -21,4 +21,5 @@ class NullAuthenticator : IAuthenticator { override fun isLegacyLoggedIn(): Boolean = false + override fun isDataPendingUpload(): Boolean = false } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index e0ce7b2c..98a2e3d9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -7,11 +7,13 @@ import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent import com.facebook.litho.annotations.Prop +import com.facebook.litho.widget.Progress import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT_MEDIUM import com.maubis.scarlet.base.core.folder.FolderBuilder @@ -26,6 +28,7 @@ import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.specs.bottomBarCard import com.maubis.scarlet.base.support.specs.bottomBarRoundIcon import com.maubis.scarlet.base.support.ui.ColorUtil +import com.maubis.scarlet.base.support.ui.ThemeColorType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -167,3 +170,40 @@ object MainActivityDisabledSyncSpec { onClick() } } + +@LayoutSpec +object MainActivitySyncingNowSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext): Component { + val colorConfig = ToolbarColorConfig( + toolbarBackgroundColor = instance.themeController().get(ThemeColorType.TOOLBAR_BACKGROUND), + toolbarIconColor = instance.themeController().get(ThemeColorType.TOOLBAR_ICON) + ) + val row = Row.create(context) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 4f) + .child(bottomBarRoundIcon(context, colorConfig) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.icon_folder_sync)) + .child(Text.create(context) + .typeface(FONT_MONSERRAT) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .textRes(R.string.home_syncing_top_layout) + .textSizeRes(R.dimen.font_size_normal) + .textColor(colorConfig.toolbarIconColor)) + .child( + Progress.create(context) + .widthDip(48f) + .paddingDip(YogaEdge.ALL, 8f) + .color(instance.themeController().get(ThemeColorType.ACCENT_TEXT))) + .clickHandler(MainActivityDisabledSync.onClickEvent(context)) + return bottomBarCard(context, row.build(), colorConfig).build() + } + + @OnEvent(ClickEvent::class) + fun onClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} diff --git a/base/src/main/res/layout/activity_main.xml b/base/src/main/res/layout/activity_main.xml index 3b6b7864..21eef020 100644 --- a/base/src/main/res/layout/activity_main.xml +++ b/base/src/main/res/layout/activity_main.xml @@ -42,39 +42,6 @@ android:layout_height="wrap_content" /> - - - - - - - - + + @Query("SELECT * FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getAllPending(): List + + @Query("SELECT * FROM gdrive_upload WHERE type = :type AND (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getPendingByType(type: String): List } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt index eb9c6963..51d355c8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt @@ -8,12 +8,14 @@ import android.content.Context var gDriveDatabase: GDriveUploadDataDao? = null fun genGDriveUploadDatabase(context: Context): GDriveUploadDataDao? { if (gDriveDatabase === null) { - gDriveDatabase = Room.databaseBuilder(context, GDriveUploadDatabase::class.java, "google_drive_db").build().drive() + gDriveDatabase = Room.databaseBuilder(context, GDriveUploadDatabase::class.java, "google_drive_db") + .fallbackToDestructiveMigration() + .build().drive() } return gDriveDatabase } -@Database(entities = [GDriveUploadData::class], version = 1) +@Database(entities = [GDriveUploadData::class], version = 2) abstract class GDriveUploadDatabase : RoomDatabase() { abstract fun drive(): GDriveUploadDataDao } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt index 4debe779..c81f2ef7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -29,6 +29,8 @@ class GDriveAuthenticator { } } + fun isDataPendingUpload(): Boolean = gDrive?.isDataPendingUpload() ?: false + fun isLoggedIn(context: Context): Boolean { if (hasAccountSetup.get()) { return account !== null diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 99655775..2576b14a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -20,9 +20,11 @@ import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.data.* import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.support.utils.log import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicBoolean const val FOLDER_NAME_IMAGES = "images" const val FOLDER_NAME_NOTES = "notes" @@ -83,6 +85,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var foldersSync: GDriveRemoteFolder? = null private var tagsSync: GDriveRemoteFolder? = null private var imageSync: GDriveRemoteImageFolder? = null + private var syncing = HashMap() fun init(helper: GDriveServiceHelper) { val context = weakContext.get() @@ -94,6 +97,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { driveHelper = helper gDriveDatabase = genGDriveUploadDatabase(context) + syncing[GDriveDataType.NOTE] = AtomicBoolean(false) + syncing[GDriveDataType.TAG] = AtomicBoolean(false) + syncing[GDriveDataType.FOLDER] = AtomicBoolean(false) + syncing[GDriveDataType.NOTE_META] = AtomicBoolean(false) + syncing[GDriveDataType.IMAGE] = AtomicBoolean(false) + notesSync = GDriveRemoteFolder( dataType = GDriveDataType.NOTE, database = gDriveDatabase!!, @@ -244,6 +253,22 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } + /** + * Pending Upload + */ + fun isDataPendingUpload(): Boolean { + val database = gDriveDatabase + if (database === null) { + return false + } + + val pending = database.getAllPending() + pending.forEach { + log("GDrivRemoe", "pending(${it.uuid}, ${it.type})") + } + return pending.isNotEmpty() + } + /** * Notify local changes to the notes */ @@ -322,14 +347,13 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { */ @Synchronized - fun resync(force: Boolean, onSyncCompleted: () -> Unit) { + fun resync(onSyncCompleted: () -> Unit) { if (!isValidController) { onSyncCompleted() return } - if (!force && sGDriveLastSync > getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { - onSyncCompleted() + if (sGDriveLastSync >= getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { return } sGDriveLastSync = getTrueCurrentTime() @@ -345,7 +369,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } fun resyncDataSync(type: GDriveDataType) { - gDriveDatabase?.getByType(type.name)?.forEach { + if (syncing[type]?.getAndSet(true) == true) { + return + } + gDriveDatabase?.getPendingByType(type.name)?.forEach { + log("GDrive", "resyncDataSync(${type.name}, ${it.uuid}, ${it.lastUpdateTimestamp}, ${it.gDriveUpdateTimestamp})") + log("GDrive", "resyncDataSync(${type.name}, ${it.uuid}, ${it.localStateDeleted}, ${it.gDriveStateDeleted})") val sameDelete = it.localStateDeleted == it.gDriveStateDeleted val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp if (!sameUpdateTime) { @@ -357,6 +386,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } } + syncing[type]?.set(false) } /** @@ -370,7 +400,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return@launch } + log("GDrive", "localDatabaseUpdate(${itemType.name}, $itemUUID)") val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() + log("GDrive", "existing(${existing.uuid}, ${existing.lastUpdateTimestamp}, ${existing.gDriveUpdateTimestamp})") existing.apply { uuid = itemUUID type = itemType.name @@ -388,7 +420,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return@launch } + log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() + log("GDrive", "existing(${existing.uuid}, ${existing.lastUpdateTimestamp}, ${existing.gDriveUpdateTimestamp})") existing.apply { uuid = itemUUID type = itemType.name @@ -496,7 +530,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { temporaryNote.mergeMetas(item) IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) + remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) } catch (exception: Exception) { } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index fd5fefba..7218b9e7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -32,10 +32,29 @@ abstract class GDriveRemoteFolderBase( return@launch } + if (uploadData.gDriveStateDeleted != deleted) { + uploadData.apply { + gDriveUpdateTimestamp = modifiedTime + fileId = uid + gDriveStateDeleted = deleted + save(database) + } + return@launch + } + + if (uploadData.fileId != uid && uploadData.gDriveUpdateTimestamp < modifiedTime) { + // This is a bit of shit situation to be in, as multiple files are pointing to the + // same name... Something Google Drive allows for whatever reason + // To help disambiguate, choose the one with the higher update timestamp + uploadData.apply { + gDriveUpdateTimestamp = modifiedTime + fileId = uid + gDriveStateDeleted = deleted + save(database) + } + } - if (uploadData.gDriveUpdateTimestamp != modifiedTime - || uploadData.fileId != uid - || uploadData.gDriveStateDeleted != deleted) { + if (uploadData.fileId == uid && uploadData.gDriveUpdateTimestamp != modifiedTime) { uploadData.apply { gDriveUpdateTimestamp = modifiedTime fileId = uid diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 957a7a90..ae0c953b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -22,7 +22,6 @@ var sFirebaseKilled: Boolean set(value) = Scarlet.gDriveConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit class ScarletAuthenticator() : IAuthenticator { - val firebase = FirebaseAuthenticator() val gdrive = GDriveAuthenticator() @@ -60,6 +59,13 @@ class ScarletAuthenticator() : IAuthenticator { firebase.logout() } + override fun isDataPendingUpload(): Boolean { + if (sGDriveLoggedIn) { + return gdrive.isDataPendingUpload() + } + return false + } + override fun openLoginActivity(context: Context) = Runnable { context.startActivity(Intent(context, GDriveLoginActivity::class.java)) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index d4ed67a0..1feee1a4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -39,11 +39,11 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { openIfNeeded(activity) } - override fun resyncDrive(force: Boolean, onSyncCompleted: () -> Unit) { + override fun resyncDrive(onSyncCompleted: () -> Unit) { if (gDrive === null) { onSyncCompleted() return } - gDrive?.resync(force, onSyncCompleted) + gDrive?.resync(onSyncCompleted) } } \ No newline at end of file From c29d818675f792fb5847ac1e87841479a51829ee Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 2 Jun 2019 19:17:00 +0100 Subject: [PATCH 033/134] [GDrive] Better Pending Changes Indicator --- .../com/maubis/scarlet/base/MainActivity.kt | 43 ++++++++---------- .../maubis/scarlet/base/config/CoreConfig.kt | 2 - .../scarlet/base/config/MaterialNoteConfig.kt | 2 - .../base/config/auth/IAuthenticator.kt | 4 +- .../config/auth/IPendingUploadListener.kt | 8 ++++ .../base/config/auth/NullAuthenticator.kt | 2 +- .../main/specs/MainActivityBottomBarSpec.kt | 20 ++++----- .../quicknote/database/GDriveUploadData.kt | 4 +- .../quicknote/drive/GDriveAuthenticator.kt | 2 - .../quicknote/drive/GDriveRemoteDatabase.kt | 44 ++++++++++++------- .../quicknote/drive/GDriveServiceHelper.kt | 7 ++- .../quicknote/scarlet/ScarletAuthenticator.kt | 7 +-- .../quicknote/scarlet/ScarletConfig.kt | 8 ---- 13 files changed, 80 insertions(+), 73 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 9eaef989..be1fa5eb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -15,6 +15,7 @@ import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag @@ -324,38 +325,26 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { .build())) } - fun notifySyncingInformation() { + fun notifySyncingInformation(isSyncPending: Boolean) { val componentContext = ComponentContext(this) if (!instance.authenticator().isLoggedIn(this) || instance.authenticator().isLegacyLoggedIn()) { return } - GlobalScope.launch { - if (!instance.authenticator().isDataPendingUpload()) { - GlobalScope.launch(Dispatchers.Main) { - lithoPreBottomToolbar.removeAllViews() - } - return@launch - } - + if (!isSyncPending) { GlobalScope.launch(Dispatchers.Main) { lithoPreBottomToolbar.removeAllViews() - lithoPreBottomToolbar.addView(LithoView.create(componentContext, - MainActivitySyncingNow.create(componentContext) - .onClick {} - .build())) - - val weakActivity = WeakReference(this@MainActivity) - ApplicationBase.instance.resyncDrive { - GlobalScope.launch(Dispatchers.Main) { - val instance = weakActivity.get() - if (instance !== null && !instance.isDestroyed) { - instance.notifySyncingInformation() - } - } - } } + return + } + + GlobalScope.launch(Dispatchers.Main) { + lithoPreBottomToolbar.removeAllViews() + lithoPreBottomToolbar.addView(LithoView.create(componentContext, + MainActivitySyncingNow.create(componentContext) + .onClick {} + .build())) } } @@ -380,7 +369,12 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { registerNoteReceiver() notifyDisabledSync() - notifySyncingInformation() + + instance.authenticator().setPendingUploadListener(object : IPendingUploadListener { + override fun onPendingStateUpdate(isDataSyncPending: Boolean) { + notifySyncingInformation(isDataSyncPending) + } + }) } fun resetAndSetupData() { @@ -445,6 +439,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { override fun onPause() { super.onPause() unregisterReceiver(receiver) + instance.authenticator().setPendingUploadListener(null) } override fun onDestroy() { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 66d253cf..4cc24d81 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -65,8 +65,6 @@ abstract class CoreConfig(context: Context) { abstract fun imageCache(): ImageCache - abstract fun resyncDrive(onSyncCompleted: () -> Unit) - companion object { val notesDb get() = instance.notesDatabase() val tagsDb get() = instance.tagsDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 95e28d21..4ea9b0ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -66,6 +66,4 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun store(): Store = store override fun imageCache(): ImageCache = imageCache - - override fun resyncDrive(onSyncCompleted: () -> Unit) = onSyncCompleted() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index 2a9357b3..c86e2b47 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -10,8 +10,6 @@ interface IAuthenticator { fun isLegacyLoggedIn(): Boolean - fun isDataPendingUpload(): Boolean - fun userId(context: Context): String? fun openLoginActivity(context: Context): Runnable? @@ -22,5 +20,7 @@ interface IAuthenticator { fun openLogoutActivity(context: Context): Runnable? + fun setPendingUploadListener(listener: IPendingUploadListener?) + fun logout() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt new file mode 100644 index 00000000..fcafd3a7 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt @@ -0,0 +1,8 @@ +package com.maubis.scarlet.base.config.auth + +interface IPendingUploadListener { + /** + * Fires when the pending state changes. + */ + fun onPendingStateUpdate(isDataSyncPending: Boolean) +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index 8ef72026..d31b9c78 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -21,5 +21,5 @@ class NullAuthenticator : IAuthenticator { override fun isLegacyLoggedIn(): Boolean = false - override fun isDataPendingUpload(): Boolean = false + override fun setPendingUploadListener(listener: IPendingUploadListener?) {} } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 98a2e3d9..87026dc5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -183,21 +183,21 @@ object MainActivitySyncingNowSpec { .widthPercent(100f) .alignItems(YogaAlign.CENTER) .paddingDip(YogaEdge.HORIZONTAL, 4f) - .child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.icon_folder_sync)) + .child(Row.create(context) + .child(EmptySpec.create(context).flexGrow(1f)) + .child( + Progress.create(context) + .widthDip(48f) + .paddingDip(YogaEdge.ALL, 8f) + .marginDip(YogaEdge.HORIZONTAL, 8f) + .color(instance.themeController().get(ThemeColorType.ACCENT_TEXT))) + .flexGrow(1f)) .child(Text.create(context) .typeface(FONT_MONSERRAT) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .flexGrow(1f) .textRes(R.string.home_syncing_top_layout) .textSizeRes(R.dimen.font_size_normal) .textColor(colorConfig.toolbarIconColor)) - .child( - Progress.create(context) - .widthDip(48f) - .paddingDip(YogaEdge.ALL, 8f) - .color(instance.themeController().get(ThemeColorType.ACCENT_TEXT))) + .child(EmptySpec.create(context).flexGrow(1f)) .clickHandler(MainActivityDisabledSync.onClickEvent(context)) return bottomBarCard(context, row.build(), colorConfig).build() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index 2196af24..56cd4c2f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -61,8 +61,8 @@ interface GDriveUploadDataDao { @Query("SELECT * FROM gdrive_upload WHERE type = :type") fun getByType(type: String): List - @Query("SELECT * FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") - fun getAllPending(): List + @Query("SELECT COUNT(*) FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getPendingCount(): Int @Query("SELECT * FROM gdrive_upload WHERE type = :type AND (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") fun getPendingByType(type: String): List diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt index c81f2ef7..4debe779 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -29,8 +29,6 @@ class GDriveAuthenticator { } } - fun isDataPendingUpload(): Boolean = gDrive?.isDataPendingUpload() ?: false - fun isLoggedIn(context: Context): Boolean { if (hasAccountSetup.get()) { return account !== null diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 2576b14a..a4df363d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -11,6 +11,7 @@ import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.getFormats @@ -87,6 +88,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var imageSync: GDriveRemoteImageFolder? = null private var syncing = HashMap() + private var lastSyncState: AtomicBoolean = AtomicBoolean(true) + private var syncListener: IPendingUploadListener? = null + fun init(helper: GDriveServiceHelper) { val context = weakContext.get() if (context === null) { @@ -256,17 +260,26 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { /** * Pending Upload */ - fun isDataPendingUpload(): Boolean { - val database = gDriveDatabase - if (database === null) { - return false - } - val pending = database.getAllPending() - pending.forEach { - log("GDrivRemoe", "pending(${it.uuid}, ${it.type})") + fun setPendingUploadListener(listener: IPendingUploadListener?) { + syncListener = listener + verifyAndNotifyPendingStateChange() + resync() + } + + fun verifyAndNotifyPendingStateChange() { + GlobalScope.launch { + val database = gDriveDatabase + if (database === null) { + return@launch + } + + val currentPendingState = database.getPendingCount() > 0 + if (currentPendingState != lastSyncState.get()) { + lastSyncState.set(currentPendingState) + syncListener?.onPendingStateUpdate(lastSyncState.get()) + } } - return pending.isNotEmpty() } /** @@ -347,16 +360,17 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { */ @Synchronized - fun resync(onSyncCompleted: () -> Unit) { + fun resync() { if (!isValidController) { - onSyncCompleted() return } + /** if (sGDriveLastSync >= getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { return } sGDriveLastSync = getTrueCurrentTime() + **/ GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) @@ -364,7 +378,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { resyncDataSync(GDriveDataType.TAG) resyncDataSync(GDriveDataType.FOLDER) resyncDataSync(GDriveDataType.IMAGE) - onSyncCompleted() } } @@ -374,7 +387,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } gDriveDatabase?.getPendingByType(type.name)?.forEach { log("GDrive", "resyncDataSync(${type.name}, ${it.uuid}, ${it.lastUpdateTimestamp}, ${it.gDriveUpdateTimestamp})") - log("GDrive", "resyncDataSync(${type.name}, ${it.uuid}, ${it.localStateDeleted}, ${it.gDriveStateDeleted})") val sameDelete = it.localStateDeleted == it.gDriveStateDeleted val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp if (!sameUpdateTime) { @@ -402,7 +414,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { log("GDrive", "localDatabaseUpdate(${itemType.name}, $itemUUID)") val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() - log("GDrive", "existing(${existing.uuid}, ${existing.lastUpdateTimestamp}, ${existing.gDriveUpdateTimestamp})") existing.apply { uuid = itemUUID type = itemType.name @@ -410,6 +421,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { localStateDeleted = removed save(database) } + + verifyAndNotifyPendingStateChange() } } @@ -422,7 +435,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() - log("GDrive", "existing(${existing.uuid}, ${existing.lastUpdateTimestamp}, ${existing.gDriveUpdateTimestamp})") existing.apply { uuid = itemUUID type = itemType.name @@ -430,6 +442,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { localStateDeleted = gDriveStateDeleted save(database) } + + verifyAndNotifyPendingStateChange() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 38b04b69..34e2b57a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -34,6 +34,8 @@ var lastCheckpointTime: AtomicLong = AtomicLong(0L) var numQueriesSinceLastCheckpoint: AtomicLong = AtomicLong(0L) class ErrorCallable(val action: String, val callable: Callable) : Callable { + private var delay: Long = 100L + override fun call(): T? { val lastCheckpoint = lastCheckpointTime.get() if (lastCheckpoint == 0L) { @@ -48,7 +50,8 @@ class ErrorCallable(val action: String, val callable: Callable) : Callable when { currentQueriesPerSecond > MAX_THRESHOLD_QUERIES_PER_SECOND -> { log("GDrive", "Rate limiting measures taken: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS") - SystemClock.sleep(500L) + delay *= 2 + SystemClock.sleep(delay) return call() } (currentCount / deltaTimeS) < MIN_RESET_QUERIES_PER_SECOND -> { @@ -81,7 +84,7 @@ fun getTrueCurrentTime(): Long { } class GDriveServiceHelper(private val mDriveService: Drive) { - private val mExecutor = Executors.newFixedThreadPool(2) + private val mExecutor = Executors.newFixedThreadPool(4) fun execute(action: String = "", callable: Callable): Task { return Tasks.call(mExecutor, ErrorCallable(action, callable)) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index ae0c953b..b1b96a9c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -3,6 +3,7 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context import android.content.Intent import com.bijoysingh.quicknote.Scarlet +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.drive.GDriveAuthenticator import com.bijoysingh.quicknote.drive.GDriveLoginActivity import com.bijoysingh.quicknote.drive.GDriveLogoutActivity @@ -10,6 +11,7 @@ import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator import com.maubis.scarlet.base.config.auth.IAuthenticator +import com.maubis.scarlet.base.config.auth.IPendingUploadListener const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" var sGDriveLoggedIn: Boolean @@ -59,11 +61,10 @@ class ScarletAuthenticator() : IAuthenticator { firebase.logout() } - override fun isDataPendingUpload(): Boolean { + override fun setPendingUploadListener(listener: IPendingUploadListener?) { if (sGDriveLoggedIn) { - return gdrive.isDataPendingUpload() + gDrive?.setPendingUploadListener(listener) } - return false } override fun openLoginActivity(context: Context) = Runnable { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index 1feee1a4..cea4f87e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -38,12 +38,4 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { override fun startListener(activity: AppCompatActivity) { openIfNeeded(activity) } - - override fun resyncDrive(onSyncCompleted: () -> Unit) { - if (gDrive === null) { - onSyncCompleted() - return - } - gDrive?.resync(onSyncCompleted) - } } \ No newline at end of file From 9b5d200c603ce4f4001bbb888c01f5757182cf5a Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 2 Jun 2019 19:42:23 +0100 Subject: [PATCH 034/134] [Exceptions] Making the application throw exceptions in debug builds --- .../scarlet/base/config/ApplicationBase.kt | 5 +++- .../scarlet/base/core/format/FormatBuilder.kt | 3 ++ .../scarlet/base/core/note/NoteExtensions.kt | 7 +++-- .../scarlet/base/core/note/NoteImage.kt | 3 +- .../base/export/remote/RemoteFolder.kt | 3 ++ .../base/export/remote/RemoteImagesFolder.kt | 2 ++ .../base/export/support/NoteImporter.kt | 7 +++-- .../main/recycler/InformationRecyclerItem.kt | 4 ++- .../recycler/ToolbarMainRecyclerHolder.kt | 4 ++- .../creation/activity/CreateNoteActivity.kt | 4 +-- .../formats/recycler/FormatImageViewHolder.kt | 3 ++ .../formats/recycler/FormatTextViewHolder.kt | 5 ++-- .../base/note/reminders/ReminderJob.kt | 4 ++- .../notification/NotificationIntentService.kt | 5 ++-- .../base/service/FloatingNoteService.kt | 3 +- .../sheet/AboutSettingsOptionsBottomSheet.kt | 2 ++ .../base/settings/sheet/AboutUsBottomSheet.kt | 3 ++ .../settings/sheet/OpenSourceBottomSheet.kt | 2 ++ .../base/support/database/MigrationUtils.kt | 4 ++- .../scarlet/base/support/ui/ThemeManager.kt | 5 ++-- .../scarlet/base/support/ui/ThemedActivity.kt | 5 ++-- .../base/support/utils/ExceptionUtils.kt | 30 +++++++++++++++++++ .../base/support/utils/FlavourUtils.kt | 2 +- .../scarlet/base/support/utils/ImageCache.kt | 2 +- .../quicknote/drive/GDriveLoginActivity.kt | 3 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 21 ++++++------- .../quicknote/drive/GDriveRemoteFolder.kt | 2 ++ .../quicknote/drive/GDriveServiceHelper.kt | 11 ++++--- .../activity/FirebaseLoginActivity.kt | 4 +-- .../firebase/activity/ForgetMeActivity.kt | 3 +- .../firebase/support/FirebaseAuthenticator.kt | 7 +++-- .../support/FirebaseFolderReference.kt | 5 ++-- .../firebase/support/FirebaseNoteReference.kt | 5 ++-- .../firebase/support/FirebaseTagReference.kt | 5 ++-- .../firebase/support/RemoteConfigFetcher.kt | 2 ++ 35 files changed, 132 insertions(+), 53 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index 5cc1f82d..372b4730 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -6,6 +6,7 @@ import com.facebook.soloader.SoLoader import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator +import com.maubis.scarlet.base.support.utils.maybeThrow import java.lang.Exception abstract class ApplicationBase : Application() { @@ -14,7 +15,9 @@ abstract class ApplicationBase : Application() { SoLoader.init(this, false) try { JobManager.create(this).addJobCreator(ReminderJobCreator()) - } catch (exception: Exception) {} + } catch (exception: Exception) { + maybeThrow(exception) + } noteImagesFolder = NoteImage(this) } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt index f4b86b23..7f727b30 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.core.format import com.maubis.markdown.segmenter.MarkdownSegmentType import com.maubis.scarlet.base.note.toInternalFormats +import com.maubis.scarlet.base.support.utils.maybeThrow import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -57,9 +58,11 @@ class FormatBuilder { format.uid = formats.size formats.add(format) } catch (innerException: JSONException) { + maybeThrow(innerException) } } } catch (exception: Exception) { + maybeThrow(exception) } return formats } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt index 0474728a..d516ace3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt @@ -5,6 +5,7 @@ import com.google.gson.Gson import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.support.utils.throwOrReturn import java.util.* fun Note.isUnsaved(): Boolean { @@ -34,15 +35,15 @@ fun Note.getNoteState(): NoteState { try { return NoteState.valueOf(this.state) } catch (exception: Exception) { - return NoteState.DEFAULT + return throwOrReturn(exception, NoteState.DEFAULT) } } fun Note.getMeta(): NoteMeta { try { return Gson().fromJson(this.meta, NoteMeta::class.java) ?: NoteMeta() - } catch (e: Exception) { - return NoteMeta() + } catch (exception: Exception) { + return throwOrReturn(exception, NoteMeta()) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt index 05729fc6..7baf08cc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt @@ -9,6 +9,7 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -38,7 +39,7 @@ class NoteImage(context: Context) { fun getFile(noteUUID: String, imageFormat: Format): File { if (imageFormat.formatType != FormatType.IMAGE) { - throw IllegalArgumentException("Format should be an Image") + maybeThrow("Format should be an Image") } return getFile(noteUUID, imageFormat.text) } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt index dd364f25..ac444c2a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt @@ -4,6 +4,7 @@ import com.github.bijoysingh.starter.util.FileManager import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.support.KEY_EXTERNAL_FOLDER_SYNC_LAST_SCAN +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -37,6 +38,7 @@ class RemoteFolder(val folder: File, onRemoteInsert(item) } } catch (exception: Exception) { + maybeThrow(exception) } } } @@ -64,6 +66,7 @@ class RemoteFolder(val folder: File, val file = file(uuid) FileManager.writeToFile(file, data) } catch (exception: Exception) { + maybeThrow(exception) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt index d1828ac4..204f7bcb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt @@ -5,6 +5,7 @@ import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.support.utils.ImageCache +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.io.File @@ -107,6 +108,7 @@ class RemoteImagesFolder(context: Context, val folder: File) { inStream.close() outStream.close() } catch (exception: Exception) { + maybeThrow(exception) } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt index f9823b68..e22b46fa 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt @@ -13,6 +13,8 @@ import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.note.folder.saveIfUnique import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.note.tag.saveIfUnique +import com.maubis.scarlet.base.support.utils.maybeThrow +import com.maubis.scarlet.base.support.utils.throwOrReturn import org.json.JSONArray import java.io.BufferedReader import java.io.File @@ -58,6 +60,7 @@ class NoteImporter() { } } catch (exception: Exception) { importNoteFallback(content, context) + maybeThrow(exception) } } @@ -100,7 +103,7 @@ class NoteImporter() { files.addAll(childFile) } } catch (exception: Exception) { - // Failed + maybeThrow(exception) } return files @@ -120,7 +123,7 @@ class NoteImporter() { return fileContents.toString() } catch (exception: IOException) { reader.close() - return "" + return throwOrReturn(exception, "") } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt index 5f87d24c..a3cf4bce 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt @@ -15,6 +15,7 @@ import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.utils.Flavor import com.maubis.scarlet.base.support.utils.FlavourUtils +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async @@ -166,8 +167,9 @@ fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem try { context.startActivity(intent) ApplicationBase.instance.store().put(KEY_MIGRATE_TO_PRO_SUCCESS, true) - } catch (e: Exception) { + } catch (exception: Exception) { ToastHelper.show(context, "Failed transferring notes to Scarlet Pro") + maybeThrow(exception) } } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index 01bd9362..8af6e155 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -17,6 +17,7 @@ import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.visibility +import com.maubis.scarlet.base.support.utils.maybeThrow class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder(context, itemView) { @@ -54,6 +55,7 @@ fun RecyclerViewHolder.setFullSpan() { try { val layoutParams = itemView.getLayoutParams() as StaggeredGridLayoutManager.LayoutParams layoutParams.isFullSpan = true - } catch (e: Exception) { + } catch (exception: Exception) { + maybeThrow(exception) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index c49795a5..c1be57b6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -141,8 +141,8 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { triggerImageLoaded(index, targetFile) } - override fun onImagePickerError(e: Exception, source: EasyImage.ImageSource, type: Int) { - //Some error handling + override fun onImagePickerError(exception: Exception, source: EasyImage.ImageSource, type: Int) { + maybeThrow(exception) } }) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index 726cd0ca..ede0a528 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -15,6 +15,7 @@ import com.maubis.scarlet.base.core.note.ImageLoadCallback import com.maubis.scarlet.base.main.sheets.openDeleteFormatDialog import com.maubis.scarlet.base.note.creation.sheet.FormatActionBottomSheet import com.maubis.scarlet.base.support.ui.visibility +import com.maubis.scarlet.base.support.utils.maybeThrow import pl.aprilapps.easyphotopicker.EasyImage import java.io.File @@ -55,6 +56,7 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase EasyImage.openCamera(context as AppCompatActivity, data.uid) } catch (e: Exception) { ToastHelper.show(context, "No camera app installed") + maybeThrow(e) } } actionGallery.setOnClickListener { @@ -62,6 +64,7 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase EasyImage.openGallery(context as AppCompatActivity, data.uid) } catch (e: Exception) { ToastHelper.show(context, "No photo picker app installed") + maybeThrow(e) } } actionMove.setColorFilter(config.iconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt index c9186c74..84693621 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt @@ -20,6 +20,7 @@ import com.maubis.scarlet.base.note.creation.sheet.FormatActionBottomSheet import com.maubis.scarlet.base.note.creation.sheet.sEditorLiveMarkdown import com.maubis.scarlet.base.note.creation.sheet.sEditorMoveHandles import com.maubis.scarlet.base.support.ui.visibility +import com.maubis.scarlet.base.support.utils.maybeThrow open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolderBase(context, view), TextWatcher { @@ -126,8 +127,8 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder try { val additionTokenLength = (if (markdownType.requiresNewLine) 1 else 0) + markdownType.startToken.length edit.setSelection(Math.min(startString.length + additionTokenLength, edit.text.length)) - } catch (_: Exception) { - // Ignore the exception + } catch (exception: Exception) { + maybeThrow(exception) } } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt index 11ec928a..29675dee 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt @@ -13,6 +13,7 @@ import com.maubis.scarlet.base.note.saveWithoutSync import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.notification.REMINDER_NOTIFICATION_CHANNEL_ID +import com.maubis.scarlet.base.support.utils.maybeThrow import java.util.* import java.util.concurrent.TimeUnit @@ -43,7 +44,8 @@ class ReminderJob : Job() { note.meta = "" note.saveWithoutSync(context) } - } catch (e: Exception) { + } catch (exception: Exception) { + maybeThrow(exception) } return Job.Result.SUCCESS diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt index d2052e94..c966dae2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationIntentService.kt @@ -8,6 +8,7 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.support.INTENT_KEY_ACTION import com.maubis.scarlet.base.support.INTENT_KEY_NOTE_ID +import com.maubis.scarlet.base.support.utils.throwOrReturn class NotificationIntentService : IntentService("NotificationIntentService") { @@ -54,8 +55,8 @@ class NotificationIntentService : IntentService("NotificationIntentService") { try { return NoteAction.valueOf(action) - } catch (_: Exception) { - return null + } catch (exception: Exception) { + return throwOrReturn(exception, null) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt index 9586c8bc..02ae78c3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt @@ -21,6 +21,7 @@ import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.maybeThrow /** * The floating not service @@ -90,7 +91,7 @@ class FloatingNoteService : FloatingBubbleService() { intent.addFlags(FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } catch (exception: Exception) { - // Some issue + maybeThrow(exception) } stopSelf() } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt index 2b72ce57..79e1910c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt @@ -13,6 +13,7 @@ import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.maybeThrow const val PRIVACY_POLICY_LINK = "https://www.iubenda.com/privacy-policy/8213521" @@ -49,6 +50,7 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SettingsOptionsBottomSheet.GITHUB_FAQ_URL))) dismiss() } catch (exception: Exception) { + maybeThrow(exception) } } )) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index abd59e23..f183f794 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -15,6 +15,7 @@ import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.maybeThrow class AboutUsBottomSheet : LithoBottomSheet() { @@ -26,6 +27,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { val pInfo = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0) version = pInfo.versionName } catch (exception: Exception) { + maybeThrow(exception) } val appName = getString(R.string.app_name) @@ -73,6 +75,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { IntentUtils.openAppPlayStore(activity) dismiss() } catch (exception: Exception) { + maybeThrow(exception) } }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index a6ec5d2c..2b899ad8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -17,6 +17,7 @@ import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.maybeThrow class OpenSourceBottomSheet : LithoBottomSheet() { @@ -55,6 +56,7 @@ class OpenSourceBottomSheet : LithoBottomSheet() { activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_URL))) dismiss() } catch (exception: Exception) { + maybeThrow(exception) } } .paddingDip(YogaEdge.VERTICAL, 8f)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 46406206..0b032f67 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -14,6 +14,7 @@ import com.maubis.scarlet.base.support.ui.KEY_APP_THEME import com.maubis.scarlet.base.support.ui.KEY_NIGHT_THEME import com.maubis.scarlet.base.support.ui.Theme import com.maubis.scarlet.base.support.utils.getLastUsedAppVersionCode +import com.maubis.scarlet.base.support.utils.maybeThrow import java.io.File import java.util.* @@ -71,7 +72,8 @@ class Migrator(val context: Context) { try { task() - } catch (_: Exception) { + } catch (exception: Exception) { + maybeThrow(exception) } ApplicationBase.instance.store().put(key, true) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index 9ca1d90d..27f0e7ef 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -8,6 +8,7 @@ import com.github.bijoysingh.starter.util.DimensionManager import com.maubis.markdown.MarkdownConfig import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.support.utils.throwOrReturn const val KEY_APP_THEME = "KEY_APP_THEME" @@ -97,8 +98,8 @@ class ThemeManager() : IThemeManager { val theme = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) try { return Theme.valueOf(theme) - } catch (_: Exception) { - return Theme.DARK + } catch (exception: Exception) { + return throwOrReturn(exception, Theme.DARK) } } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index a2ae1bad..0c737db8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -6,6 +6,7 @@ import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.support.utils.maybeThrow abstract class ThemedActivity : AppCompatActivity() { @@ -43,7 +44,7 @@ abstract class ThemedActivity : AppCompatActivity() { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0) } catch (exception: Exception) { - // Do nothing + maybeThrow(exception) } } @@ -52,7 +53,7 @@ abstract class ThemedActivity : AppCompatActivity() { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) } catch (exception: Exception) { - // Do nothing + maybeThrow(exception) } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt new file mode 100644 index 00000000..7cfad6b9 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt @@ -0,0 +1,30 @@ +package com.maubis.scarlet.base.support.utils + +import com.maubis.scarlet.base.BuildConfig +import java.lang.Exception + +fun maybeThrow(message: String) { + if (BuildConfig.DEBUG) { + throw IllegalStateException(message) + } +} + +fun maybeThrow(exception: Exception) { + if (BuildConfig.DEBUG) { + throw exception + } +} + +fun throwOrReturn(message: String, result: DataType): DataType { + if (BuildConfig.DEBUG) { + throw IllegalStateException(message) + } + return result +} + +fun throwOrReturn(exception: Exception, result: DataType): DataType { + if (BuildConfig.DEBUG) { + throw exception + } + return result +} diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt index 403dd503..ed69650b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt @@ -24,7 +24,7 @@ object FlavourUtils { try { found = reference.get()?.packageManager?.getPackageInfo(PRO_APP_PACKAGE_NAME, 0) != null } catch (e: Exception) { - found = false + found = throwOrReturn(e, false) } ApplicationBase.instance.store().put(KEY_PRO_APP_INSTALLED, found) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt index a160b0e7..a494eefc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt @@ -83,7 +83,7 @@ class ImageCache(context: Context) { fOut.flush() fOut.close() } catch (exception: Exception) { - return compressedBitmap + return throwOrReturn(exception, compressedBitmap) } thumbnailCacheSize.addAndGet(cacheFile.length()) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 136e3654..44bb60a8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -29,6 +29,7 @@ import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference @@ -99,7 +100,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed return } } catch (exception: Exception) { - // Ignore this, handled by following content + maybeThrow(exception) } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index a4df363d..437c4e1f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -22,8 +22,10 @@ import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.data.* import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.utils.log +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import org.junit.Ignore import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean @@ -267,7 +269,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { resync() } - fun verifyAndNotifyPendingStateChange() { + private fun verifyAndNotifyPendingStateChange() { GlobalScope.launch { val database = gDriveDatabase if (database === null) { @@ -293,11 +295,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { when { data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid) data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) - data is Note -> notifyInsertImpl(data) + data is Note -> notifyNoteInsertImpl(data) + else -> maybeThrow("notifyInsert called with unhandled data type") } } - fun notifyInsertImpl(note: Note) { + private fun notifyNoteInsertImpl(note: Note) { val noteUuid = note.uuid localDatabaseUpdate(GDriveDataType.NOTE, noteUuid) localDatabaseUpdate(GDriveDataType.NOTE_META, noteUuid) @@ -352,6 +355,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, true) localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, true) } + else -> maybeThrow("notifyRemove called with unhandled data type") } } @@ -365,13 +369,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } - /** - if (sGDriveLastSync >= getTrueCurrentTime() - KEY_G_DRIVE_LAST_SYNC_DELTA_MS) { - return - } - sGDriveLastSync = getTrueCurrentTime() - **/ - GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) resyncDataSync(GDriveDataType.NOTE_META) @@ -529,6 +526,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) } catch (exception: Exception) { + maybeThrow(exception) } } } @@ -546,6 +544,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) } catch (exception: Exception) { + maybeThrow(exception) } } } @@ -556,6 +555,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { IRemoteDatabaseUtils.onRemoteInsert(context, item) remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) } catch (exception: Exception) { + maybeThrow(exception) } } } @@ -566,6 +566,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { IRemoteDatabaseUtils.onRemoteInsert(context, item) remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) } catch (exception: Exception) { + maybeThrow(exception) } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index ef731775..c85f30fd 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -114,6 +115,7 @@ class GDriveRemoteFolder( } } } catch (exception: Exception) { + maybeThrow(exception) } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 34e2b57a..ad62d658 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -12,6 +12,8 @@ import com.google.api.services.drive.Drive import com.google.api.services.drive.model.File import com.google.api.services.drive.model.FileList import com.maubis.scarlet.base.support.utils.log +import com.maubis.scarlet.base.support.utils.maybeThrow +import com.maubis.scarlet.base.support.utils.throwOrReturn import java.io.BufferedReader import java.io.FileOutputStream import java.io.InputStreamReader @@ -65,11 +67,7 @@ class ErrorCallable(val action: String, val callable: Callable) : Callable numQueriesSinceLastCheckpoint.addAndGet(1) return callable.call() } catch (exception: Exception) { - Log.e("GDrive", exception.message, exception) - if (BuildConfig.DEBUG) { - throw exception - } - return null + return throwOrReturn(exception, null) } } } @@ -79,6 +77,7 @@ fun getTrueCurrentTime(): Long { try { calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) } catch (exception: Exception) { + maybeThrow(exception) } return calendar.timeInMillis } @@ -151,7 +150,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { mDriveService.files().get(fileId).executeMediaAndDownloadTo(fileStream) fileStream.close() } catch (exception: Exception) { - return@Callable false + return@Callable throwOrReturn(exception, false) } destinationFile.exists() }) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt index 23605f96..1578ebe8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -28,6 +28,7 @@ import com.google.firebase.auth.GoogleAuthProvider import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.maybeThrow import java.util.concurrent.atomic.AtomicBoolean class FirebaseLoginActivity : ThemedActivity() { @@ -109,8 +110,7 @@ class FirebaseLoginActivity : ThemedActivity() { return } } catch (exception: Exception) { - Log.e("Firebase", exception.toString(), exception) - // Ignore this, handled by following content + maybeThrow(exception) } onLoginFailure() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index 6eed26b9..7815b4f5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -20,6 +20,7 @@ import com.google.firebase.auth.GoogleAuthProvider import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.bind +import com.maubis.scarlet.base.support.utils.maybeThrow class ForgetMeActivity : ThemedActivity() { @@ -117,7 +118,7 @@ class ForgetMeActivity : ThemedActivity() { return } } catch (exception: Exception) { - // Ignore this, handled by following content + maybeThrow(exception) } ToastHelper.show(this, R.string.login_to_google_failed) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt index 0cb74eac..3e6d3f6e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt @@ -13,6 +13,7 @@ import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.FirebaseDatabase import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean @@ -41,7 +42,7 @@ class FirebaseAuthenticator() { initFirebaseDatabase(context, userId!!) reloadUser(context) } catch (exception: Exception) { - // Don't need to do anything + maybeThrow(exception) } } } @@ -81,8 +82,8 @@ class FirebaseAuthenticator() { ToastHelper.show(context, "You have been signed out of the app") } } - } catch (e: Exception) { - // In case somehow it fails + } catch (exception: Exception) { + maybeThrow(exception) } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt index 8b709ed3..68e49d07 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt @@ -7,6 +7,7 @@ import com.google.firebase.database.ChildEventListener import com.google.firebase.database.DataSnapshot import com.google.firebase.database.DatabaseError import com.google.firebase.database.FirebaseDatabase +import com.maubis.scarlet.base.support.utils.maybeThrow /** @@ -53,7 +54,7 @@ fun FirebaseRemoteDatabase.setFolderListener() { } onRemoteInsert(folder) } catch (exception: Exception) { - // Ignore if exception + maybeThrow(exception) } } @@ -68,7 +69,7 @@ fun FirebaseRemoteDatabase.setFolderListener() { } onRemoteRemove(folder) } catch (exception: Exception) { - // Ignore if exception + maybeThrow(exception) } } }) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt index 2232ba3b..58f97998 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt @@ -7,6 +7,7 @@ import com.google.firebase.database.ChildEventListener import com.google.firebase.database.DataSnapshot import com.google.firebase.database.DatabaseError import com.google.firebase.database.FirebaseDatabase +import com.maubis.scarlet.base.support.utils.maybeThrow /** * Functions for Database Reference for Firebase Notes @@ -52,7 +53,7 @@ fun FirebaseRemoteDatabase.setNoteListener() { } onRemoteInsert(note) } catch (exception: Exception) { - // Ignore if exception + maybeThrow(exception) } } @@ -68,7 +69,7 @@ fun FirebaseRemoteDatabase.setNoteListener() { } onRemoteRemove(note) } catch (exception: Exception) { - // Ignore if exception + maybeThrow(exception) } } }) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt index c24e3e7c..7dd44e3f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt @@ -7,6 +7,7 @@ import com.google.firebase.database.ChildEventListener import com.google.firebase.database.DataSnapshot import com.google.firebase.database.DatabaseError import com.google.firebase.database.FirebaseDatabase +import com.maubis.scarlet.base.support.utils.maybeThrow /** @@ -54,7 +55,7 @@ fun FirebaseRemoteDatabase.setTagListener() { } onRemoteInsert(tag) } catch (exception: Exception) { - // Ignore if exception + maybeThrow(exception) } } @@ -70,7 +71,7 @@ fun FirebaseRemoteDatabase.setTagListener() { // TODO: This is disabled // onRemoteRemove(tag) } catch (exception: Exception) { - // Ignore if exception + maybeThrow(exception) } } }) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt index 0d6f3920..67430ac5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt @@ -12,6 +12,7 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.config.remote.RemoteConfig import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.maybeThrow const val REMOTE_CONFIG_URL = "https://material-notes-63563.firebaseapp.com/config/config.txt" const val REMOTE_CONFIG_REFETCH_TIME_MS = 7 * 24 * 60 * 60 * 1000 @@ -61,6 +62,7 @@ class RemoteConfigFetcher() : IRemoteConfigFetcher { ApplicationBase.instance.store().put(KEY_RC_LITE_VERSION, config.rc_lite_production_version ?: 0) ApplicationBase.instance.store().put(KEY_RC_FULL_VERSION, config.rc_full_production_version ?: 0) } catch (exception: Exception) { + maybeThrow(exception) return } } From e248e15f68e0bd85473da1ea43aeaf0c6283b180 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 2 Jun 2019 21:34:40 +0100 Subject: [PATCH 035/134] [Developer Options] Adding Developer Options --- .../com/maubis/scarlet/base/MainActivity.kt | 2 +- .../scarlet/base/core/note/NoteBuilder.kt | 1 + .../recycler/ToolbarMainRecyclerHolder.kt | 7 +- .../base/main/sheets/ExceptionBottomSheet.kt | 54 +++++++ .../scarlet/base/note/NoteExtensions.kt | 14 +- .../creation/activity/CreateNoteActivity.kt | 3 +- .../formats/recycler/FormatImageViewHolder.kt | 8 +- .../formats/recycler/FormatTextViewHolder.kt | 3 +- .../sheet/AboutSettingsOptionsBottomSheet.kt | 28 ++-- .../base/settings/sheet/AboutUsBottomSheet.kt | 4 +- .../InternalSettingsOptionsBottomSheet.kt | 61 ++++++++ .../settings/sheet/OpenSourceBottomSheet.kt | 2 +- .../scarlet/base/support/ui/ThemedActivity.kt | 4 +- .../base/support/utils/ExceptionUtils.kt | 137 ++++++++++++++++-- base/src/main/res/values/strings.xml | 20 +++ .../quicknote/drive/GDriveLoginActivity.kt | 2 +- .../quicknote/drive/GDriveServiceHelper.kt | 5 + .../activity/FirebaseLoginActivity.kt | 2 +- .../firebase/activity/ForgetMeActivity.kt | 2 +- 19 files changed, 309 insertions(+), 50 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index be1fa5eb..858a7a3a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -56,6 +56,7 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.unifiedFolderSearchSynchronous import com.maubis.scarlet.base.support.unifiedSearchSynchronous +import com.maubis.scarlet.base.support.utils.maybeThrow import com.maubis.scarlet.base.support.utils.shouldShowWhatsNewSheet import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.search_toolbar_main.* @@ -369,7 +370,6 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { registerNoteReceiver() notifyDisabledSync() - instance.authenticator().setPendingUploadListener(object : IPendingUploadListener { override fun onPendingStateUpdate(isDataSyncPending: Boolean) { notifySyncingInformation(isDataSyncPending) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt index 2cbcfff8..8f152e70 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteBuilder.kt @@ -23,6 +23,7 @@ class NoteBuilder { note.updateTimestamp = note.timestamp note.color = -0xff8695 note.folder = "" + note.description = FormatBuilder().getDescription(emptyList()) return note } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index 8af6e155..67803cd3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.main.recycler import android.content.Context import android.os.Bundle +import android.support.v7.app.AppCompatActivity import android.support.v7.widget.StaggeredGridLayoutManager import android.view.View import android.widget.ImageView @@ -11,7 +12,7 @@ import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.settings.sheet.DeleteAndMoreOptionsBottomSheet +import com.maubis.scarlet.base.settings.sheet.InternalSettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.sheets.openSheet @@ -46,7 +47,7 @@ class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerView toolbarIconDebug.visibility = visibility(BuildConfig.DEBUG) toolbarIconDebug.setColorFilter(toolbarIconColor) toolbarIconDebug.setOnClickListener { - openSheet((context as MainActivity), DeleteAndMoreOptionsBottomSheet()) + openSheet((context as MainActivity), InternalSettingsOptionsBottomSheet()) } } } @@ -56,6 +57,6 @@ fun RecyclerViewHolder.setFullSpan() { val layoutParams = itemView.getLayoutParams() as StaggeredGridLayoutManager.LayoutParams layoutParams.isFullSpan = true } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(itemView.context as AppCompatActivity, exception) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt new file mode 100644 index 00000000..93f36575 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt @@ -0,0 +1,54 @@ +package com.maubis.scarlet.base.main.sheets + +import android.app.Dialog +import android.util.Log +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.widget.Text +import com.facebook.yoga.YogaEdge +import com.maubis.markdown.Markdown +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.specs.BottomSheetBar +import com.maubis.scarlet.base.support.ui.ThemeColorType +import android.content.Intent +import android.net.Uri + +class ExceptionBottomSheet : LithoBottomSheet() { + var exception: Exception = RuntimeException() + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + val component = Column.create(componentContext) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child(getLithoBottomSheetTitle(componentContext) + .textRes(R.string.exception_sheet_title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child(Text.create(componentContext) + .textSizeRes(R.dimen.font_size_small) + .text(Markdown.render("```\n${Log.getStackTraceString(exception)}\n```", true)) + .marginDip(YogaEdge.BOTTOM, 16f) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.exception_sheet_crash_app) + .onPrimaryClick { + throw exception + }.secondaryActionRes(R.string.exception_sheet_mail) + .onSecondaryClick { + try { + val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:team.thecodershub@gmail.com")) + intent.putExtra(Intent.EXTRA_SUBJECT, "[Exception] The application threw an exception") + intent.putExtra(Intent.EXTRA_TEXT, "Hi, my app threw this exception\n${Log.getStackTraceString(exception)}") + startActivity(Intent.createChooser(intent, "Send email to developer...")) + } catch (exception: Exception) { + // Ignore this one ;) + } + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) + return component.build() + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 77759ac3..8bb9ab7c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -7,13 +7,11 @@ import com.google.gson.Gson import com.maubis.markdown.BuildConfig import com.maubis.markdown.Markdown import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType -import com.maubis.scarlet.base.core.note.NoteState -import com.maubis.scarlet.base.core.note.generateUUID -import com.maubis.scarlet.base.core.note.getFormats -import com.maubis.scarlet.base.core.note.getTagUUIDs +import com.maubis.scarlet.base.core.note.* import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet @@ -301,6 +299,14 @@ fun Note.save(context: Context) { ApplicationBase.instance.noteActions(this).save(context) } +fun Note.unsafeSave_INTERNAL_USE_ONLY() { + applySanityChecks() + + val id = CoreConfig.notesDb.database().insertNote(this) + uid = if (isUnsaved()) id.toInt() else uid + CoreConfig.notesDb.notifyInsertNote(this) +} + fun Note.saveWithoutSync(context: Context) { ApplicationBase.instance.noteActions(this).offlineSave(context) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index c1be57b6..3ede9d86 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -31,6 +31,7 @@ import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.support.recycler.SimpleItemTouchHelper import com.maubis.scarlet.base.support.specs.ToolbarColorConfig +import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.android.synthetic.main.activity_advanced_note.* import pl.aprilapps.easyphotopicker.DefaultCallback import pl.aprilapps.easyphotopicker.EasyImage @@ -142,7 +143,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { } override fun onImagePickerError(exception: Exception, source: EasyImage.ImageSource, type: Int) { - maybeThrow(exception) + maybeThrow(this@CreateNoteActivity, exception) } }) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index ede0a528..0c15020d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -54,17 +54,17 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase actionCamera.setOnClickListener { try { EasyImage.openCamera(context as AppCompatActivity, data.uid) - } catch (e: Exception) { + } catch (exception: Exception) { ToastHelper.show(context, "No camera app installed") - maybeThrow(e) + maybeThrow(context as AppCompatActivity, exception) } } actionGallery.setOnClickListener { try { EasyImage.openGallery(context as AppCompatActivity, data.uid) - } catch (e: Exception) { + } catch (exception: Exception) { ToastHelper.show(context, "No photo picker app installed") - maybeThrow(e) + maybeThrow(context as AppCompatActivity, exception) } } actionMove.setColorFilter(config.iconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt index 84693621..4a93dc58 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt @@ -1,6 +1,7 @@ package com.maubis.scarlet.base.note.formats.recycler import android.content.Context +import android.support.v7.app.AppCompatActivity import android.text.Editable import android.text.InputType import android.text.TextWatcher @@ -128,7 +129,7 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder val additionTokenLength = (if (markdownType.requiresNewLine) 1 else 0) + markdownType.startToken.length edit.setSelection(Math.min(startString.length + additionTokenLength, edit.text.length)) } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(context as AppCompatActivity, exception) } } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt index 79e1910c..0fb19f68 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt @@ -50,10 +50,19 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SettingsOptionsBottomSheet.GITHUB_FAQ_URL))) dismiss() } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(activity, exception) } } )) + options.add(LithoOptionsItem( + title = R.string.whats_new_title, + subtitle = R.string.whats_new_subtitle, + icon = R.drawable.ic_whats_new, + listener = { + openSheet(activity, WhatsNewBottomSheet()) + dismiss() + } + )) options.add(LithoOptionsItem( title = R.string.material_notes_privacy_policy, subtitle = R.string.material_notes_privacy_policy_subtitle, @@ -68,20 +77,11 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { )) options.add(LithoOptionsItem( - title = R.string.home_option_rate_and_review, - subtitle = R.string.home_option_rate_and_review_subtitle, - icon = R.drawable.ic_rating, - listener = { - IntentUtils.openAppPlayStore(activity) - dismiss() - } - )) - options.add(LithoOptionsItem( - title = R.string.whats_new_title, - subtitle = R.string.whats_new_subtitle, - icon = R.drawable.ic_whats_new, + title = R.string.internal_settings_title, + subtitle = R.string.internal_settings_description, + icon = R.drawable.icon_code_block, listener = { - com.maubis.scarlet.base.support.sheets.openSheet(activity, WhatsNewBottomSheet()) + openSheet(activity, InternalSettingsOptionsBottomSheet()) dismiss() } )) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index f183f794..c97cac24 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -27,7 +27,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { val pInfo = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0) version = pInfo.versionName } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(activity, exception) } val appName = getString(R.string.app_name) @@ -75,7 +75,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { IntentUtils.openAppPlayStore(activity) dismiss() } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(activity, exception) } }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt new file mode 100644 index 00000000..284fb0f3 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt @@ -0,0 +1,61 @@ +package com.maubis.scarlet.base.settings.sheet + +import android.app.Dialog +import com.facebook.litho.ComponentContext +import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.utils.* + +class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { + override fun title(): Int = R.string.internal_settings_title + + override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { + val activity = context as MainActivity + val options = ArrayList() + options.add(LithoOptionsItem( + title = R.string.internal_settings_enable_log_exceptions_title, + subtitle = R.string.internal_settings_enable_log_exceptions_description, + icon = R.drawable.ic_note_white_48dp, + listener = { + sInternalLogTracesToNote = !sInternalLogTracesToNote + reset(activity, dialog) + }, + isSelectable = true, + selected = sInternalLogTracesToNote + )) + options.add(LithoOptionsItem( + title = R.string.internal_settings_enable_show_exceptions_title, + subtitle = R.string.internal_settings_enable_show_exceptions_description, + icon = R.drawable.icon_add_list, + listener = { + sInternalShowTracesInSheet = !sInternalShowTracesInSheet + reset(activity, dialog) + }, + isSelectable = true, + selected = sInternalShowTracesInSheet + )) + options.add(LithoOptionsItem( + title = R.string.internal_settings_enable_throw_exceptions_title, + subtitle = R.string.internal_settings_enable_throw_exceptions_description, + icon = R.drawable.ic_whats_new, + listener = { + sInternalThrowOnException = !sInternalThrowOnException + sInternalThrownExceptionCount = 0 + reset(activity, dialog) + }, + isSelectable = true, + selected = sInternalThrowOnException + )) + options.add(LithoOptionsItem( + title = R.string.internal_settings_fake_exceptions_title, + subtitle = R.string.internal_settings_fake_exceptions_description, + icon = R.drawable.ic_info, + listener = { + maybeThrow(activity, RuntimeException("Fake Exception for Testing")) + } + )) + return options + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 2b899ad8..3953ba61 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -56,7 +56,7 @@ class OpenSourceBottomSheet : LithoBottomSheet() { activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_URL))) dismiss() } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(activity, exception) } } .paddingDip(YogaEdge.VERTICAL, 8f)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index 0c737db8..238ef3e3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -44,7 +44,7 @@ abstract class ThemedActivity : AppCompatActivity() { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0) } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(this, exception) } } @@ -53,7 +53,7 @@ abstract class ThemedActivity : AppCompatActivity() { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(this, exception) } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt index 7cfad6b9..d8433d91 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt @@ -1,30 +1,139 @@ package com.maubis.scarlet.base.support.utils -import com.maubis.scarlet.base.BuildConfig -import java.lang.Exception +import android.os.SystemClock +import android.support.v7.app.AppCompatActivity +import android.util.Log +import com.github.bijoysingh.starter.util.DateFormatter +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.core.format.Format +import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.core.note.NoteBuilder +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.core.note.isUnsaved +import com.maubis.scarlet.base.main.sheets.ExceptionBottomSheet +import com.maubis.scarlet.base.note.unsafeSave_INTERNAL_USE_ONLY +import com.maubis.scarlet.base.support.sheets.openSheet +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch -fun maybeThrow(message: String) { - if (BuildConfig.DEBUG) { - throw IllegalStateException(message) +const val KEY_INTERNAL_LOG_TRACES_TO_NOTE = "internal_log_traces_to_note" +var sInternalLogTracesToNote: Boolean + get() = instance.store().get(KEY_INTERNAL_LOG_TRACES_TO_NOTE, false) + set(value) = instance.store().put(KEY_INTERNAL_LOG_TRACES_TO_NOTE, value) + +const val KEY_INTERNAL_SHOW_TRACES_IN_SHEET = "internal_show_traces_in_sheet" +var sInternalShowTracesInSheet: Boolean + get() = instance.store().get(KEY_INTERNAL_SHOW_TRACES_IN_SHEET, false) + set(value) = instance.store().put(KEY_INTERNAL_SHOW_TRACES_IN_SHEET, value) + +const val KEY_INTERNAL_THROW_ON_EXCEPTION = "internal_throw_on_exception" +var sInternalThrowOnException: Boolean + get() = instance.store().get(KEY_INTERNAL_THROW_ON_EXCEPTION, false) + set(value) = instance.store().put(KEY_INTERNAL_THROW_ON_EXCEPTION, value) + +const val KEY_INTERNAL_THROWN_EXCEPTION_COUNT = "internal_thrown_exception_count" +var sInternalThrownExceptionCount: Int + get() = instance.store().get(KEY_INTERNAL_THROWN_EXCEPTION_COUNT, 0) + set(value) = instance.store().put(KEY_INTERNAL_THROWN_EXCEPTION_COUNT, value) + +/** + * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. + */ +fun maybeThrow(activity: AppCompatActivity, thrownException: Exception) { + if (sInternalShowTracesInSheet) { + openSheet(activity, ExceptionBottomSheet().apply { this.exception = thrownException }) } + maybeThrow(thrownException) } + +/** + * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. + */ fun maybeThrow(exception: Exception) { - if (BuildConfig.DEBUG) { - throw exception + if (sInternalLogTracesToNote) { + storeToDebugNote(Log.getStackTraceString(exception)) + } + + if (sInternalThrowOnException) { + sInternalThrownExceptionCount += 1 + if (sInternalThrownExceptionCount <= 5) { + GlobalScope.launch { + SystemClock.sleep(1000) + throw exception + } + } + + sInternalThrownExceptionCount = 0 + sInternalThrowOnException = false } } +/** + * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. + */ +fun maybeThrow(message: String) { + maybeThrow(IllegalStateException(message)) +} + +/** + * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. + * Else returns the provided value + */ fun throwOrReturn(message: String, result: DataType): DataType { - if (BuildConfig.DEBUG) { - throw IllegalStateException(message) - } - return result + return throwOrReturn(IllegalStateException(message), result) } +/** + * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. + * Else returns the provided value + */ fun throwOrReturn(exception: Exception, result: DataType): DataType { - if (BuildConfig.DEBUG) { - throw exception - } + maybeThrow(exception) return result } + + +private fun storeToDebugNote(trace: String) { + GlobalScope.launch { + storeToDebugNoteSync(trace) + } +} + +const val EXCEPTION_NOTE_KEY = "debug-note" +const val EXCEPTION_NOTE_NUM_DATA_PER_EXCEPTION = 4 +const val EXCEPTION_NOTE_MAX_EXCEPTIONS = 20 + +@Synchronized +private fun storeToDebugNoteSync(trace: String) { + val note = instance.notesDatabase().getByUUID(EXCEPTION_NOTE_KEY) + ?: NoteBuilder().emptyNote().apply { + uuid = EXCEPTION_NOTE_KEY + disableBackup = true + } + + val initialFormats = note.getFormats().toMutableList() + if (note.isUnsaved() || initialFormats.isEmpty()) { + initialFormats.add(Format(FormatType.HEADING, "Note Exceptions")) + } + + val additionalFormats = emptyList().toMutableList() + additionalFormats.add(Format(FormatType.SUB_HEADING, "Exception")) + additionalFormats.add(Format( + FormatType.QUOTE, + "Throw at ${DateFormatter.getDate(System.currentTimeMillis())}")) + additionalFormats.add(Format( + FormatType.CODE, + trace)) + additionalFormats.add(Format(FormatType.SEPARATOR)) + + val maxFormatCount = 1 + EXCEPTION_NOTE_MAX_EXCEPTIONS * EXCEPTION_NOTE_NUM_DATA_PER_EXCEPTION + if (initialFormats.size > maxFormatCount) { + initialFormats.subList(0, maxFormatCount) + } + + initialFormats.addAll(1, additionalFormats) + note.description = FormatBuilder().getDescription(initialFormats) + note.unsafeSave_INTERNAL_USE_ONLY() +} \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 19935df7..22fdb0b2 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -432,6 +432,26 @@ Syncing + + Developer Options + Change internal settings to the application used for debugging + + Log Exceptions + Log caught exceptions to a fixed note, to allow forwarding to Developer + + Show Exception Sheet + Show caught exceptions on a sheet if possible, to allow forwarding to Developer + + Throw on Exceptions + Throw and crash the application on exceptions. Will be reset after 5 crashes + + Fake Exceptions + Throw a fake exception to test the exception features + + Exception Thrown + Crash App + Mail + Connection Failed Sync on Google Drive diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 44bb60a8..a7a207cf 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -100,7 +100,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed return } } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(this, exception) } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index ad62d658..bf7fa678 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -17,6 +17,8 @@ import com.maubis.scarlet.base.support.utils.throwOrReturn import java.io.BufferedReader import java.io.FileOutputStream import java.io.InputStreamReader +import java.io.InterruptedIOException +import java.net.SocketTimeoutException import java.util.* import java.util.concurrent.Callable import java.util.concurrent.Executors @@ -66,6 +68,9 @@ class ErrorCallable(val action: String, val callable: Callable) : Callable try { numQueriesSinceLastCheckpoint.addAndGet(1) return callable.call() + } catch (exception: InterruptedIOException) { + // Ignore timeout exceptions + return null } catch (exception: Exception) { return throwOrReturn(exception, null) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt index 1578ebe8..8cf4cc61 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -110,7 +110,7 @@ class FirebaseLoginActivity : ThemedActivity() { return } } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(this, exception) } onLoginFailure() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index 7815b4f5..365af653 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -118,7 +118,7 @@ class ForgetMeActivity : ThemedActivity() { return } } catch (exception: Exception) { - maybeThrow(exception) + maybeThrow(this, exception) } ToastHelper.show(this, R.string.login_to_google_failed) } From 93bb2620d9788542b53292d4abf0082263e1e665 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 2 Jun 2019 23:57:16 +0100 Subject: [PATCH 036/134] [GDrive] Fixing sync issues and delete file bug --- .../com/maubis/scarlet/base/MainActivity.kt | 7 ++ .../main/specs/MainActivityBottomBarSpec.kt | 3 +- .../scarlet/base/note/NoteExtensions.kt | 12 ++- .../base/support/utils/ExceptionUtils.kt | 5 + .../2.json | 12 ++- .../3.json | 91 +++++++++++++++++++ .../quicknote/database/GDriveUploadData.kt | 29 ++++++ .../database/GDriveUploadDatabase.kt | 2 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 87 ++++++++++++------ .../quicknote/drive/GDriveRemoteFolder.kt | 67 ++++++++++---- .../quicknote/drive/GDriveRemoteFolderBase.kt | 12 +-- .../drive/GDriveRemoteImageFolder.kt | 3 +- .../quicknote/drive/GDriveServiceHelper.kt | 15 ++- 13 files changed, 269 insertions(+), 76 deletions(-) create mode 100644 scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 858a7a3a..c64bdd76 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -63,6 +63,7 @@ import kotlinx.android.synthetic.main.search_toolbar_main.* import kotlinx.android.synthetic.main.toolbar_trash_info.* import kotlinx.coroutines.* import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicBoolean class MainActivity : ThemedActivity(), INoteOptionSheetActivity { private val singleThreadDispatcher = newSingleThreadContext("singleThreadDispatcher") @@ -74,6 +75,8 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { private lateinit var receiver: BroadcastReceiver private lateinit var tagAndColorPicker: TagsAndColorPickerViewHolder + private var lastSyncState: AtomicBoolean = AtomicBoolean(false) + var config: SearchConfig = SearchConfig(mode = HomeNavigationState.DEFAULT) var isInSearchMode: Boolean = false @@ -333,6 +336,10 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { return } + if (lastSyncState.getAndSet(isSyncPending) == isSyncPending) { + return + } + if (!isSyncPending) { GlobalScope.launch(Dispatchers.Main) { lithoPreBottomToolbar.removeAllViews() diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 87026dc5..ec4ecf6b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -190,7 +190,8 @@ object MainActivitySyncingNowSpec { .widthDip(48f) .paddingDip(YogaEdge.ALL, 8f) .marginDip(YogaEdge.HORIZONTAL, 8f) - .color(instance.themeController().get(ThemeColorType.ACCENT_TEXT))) + .alpha(0.6f) + .color(colorConfig.toolbarIconColor)) .flexGrow(1f)) .child(Text.create(context) .typeface(FONT_MONSERRAT) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 8bb9ab7c..5e97bb6c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -86,7 +86,12 @@ fun Note.getText(): String { stringBuilder.append("\n") } } - return stringBuilder.toString().trim() + + val text = stringBuilder.toString().trim() + if (BuildConfig.DEBUG) { + return "`$uuid`\n\n$text" + } + return text } fun Note.getSmartFormats(): List { @@ -157,10 +162,7 @@ fun Note.getLockedText(isMarkdownEnabled: Boolean): CharSequence { else -> getMarkdownText(isMarkdownEnabled) } - if (BuildConfig.DEBUG) { - return "`$uuid`\n$text" - } - return text; + return text } fun Note.getDisplayTime(): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt index d8433d91..c2ea9485 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt @@ -4,6 +4,7 @@ import android.os.SystemClock import android.support.v7.app.AppCompatActivity import android.util.Log import com.github.bijoysingh.starter.util.DateFormatter +import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder @@ -68,6 +69,10 @@ fun maybeThrow(exception: Exception) { sInternalThrownExceptionCount = 0 sInternalThrowOnException = false } + + if (BuildConfig.DEBUG) { + Log.e("Scarlet", "Exception Thrown and Recovered", exception) + } } /** diff --git a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/2.json b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/2.json index bfcfa5b7..0118d0a8 100644 --- a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/2.json +++ b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/2.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "8cfc070164953172147be4a6d4ca1d2f", + "identityHash": "b21b94b1e0f1525de8c4bbe5191c125c", "entities": [ { "tableName": "gdrive_upload", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL, `attempts` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", @@ -55,6 +55,12 @@ "columnName": "gDriveStateDeleted", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "attempts", + "columnName": "attempts", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -79,7 +85,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"8cfc070164953172147be4a6d4ca1d2f\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b21b94b1e0f1525de8c4bbe5191c125c\")" ] } } \ No newline at end of file diff --git a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json new file mode 100644 index 00000000..3a835e9a --- /dev/null +++ b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json @@ -0,0 +1,91 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "b21b94b1e0f1525de8c4bbe5191c125c", + "entities": [ + { + "tableName": "gdrive_upload", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL, `attempts` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdateTimestamp", + "columnName": "lastUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localStateDeleted", + "columnName": "localStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gDriveUpdateTimestamp", + "columnName": "gDriveUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gDriveStateDeleted", + "columnName": "gDriveStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attempts", + "columnName": "attempts", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_gdrive_upload_uuid_type", + "unique": true, + "columnNames": [ + "uuid", + "type" + ], + "createSql": "CREATE UNIQUE INDEX `index_gdrive_upload_uuid_type` ON `${TABLE_NAME}` (`uuid`, `type`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b21b94b1e0f1525de8c4bbe5191c125c\")" + ] + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index 56cd4c2f..d345ee36 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -3,6 +3,7 @@ package com.bijoysingh.quicknote.database import android.arch.persistence.room.* import com.google.gson.Gson import com.maubis.scarlet.base.support.utils.log +import com.maubis.scarlet.base.support.utils.maybeThrow enum class GDriveDataType { NOTE, @@ -31,13 +32,38 @@ class GDriveUploadData { var gDriveStateDeleted: Boolean = false + var attempts: Long = 0L + @Ignore fun save(dao: GDriveUploadDataDao) { + if (uuid.isBlank() || type.isBlank()) { + maybeThrow("Invalid Dao") + return + } + + log("GDrive", "data = ${Gson().toJson(this)}") val id = dao.insert(this) uid = if (uid == 0) id.toInt() else uid } + + @Ignore + fun unsaved(): Boolean { + return uid == 0 + } } +object GDriveDatabaseHelper { + fun getByUUID(type: GDriveDataType, uuid: String): GDriveUploadData { + return getByUUID(type.name, uuid) + } + + fun getByUUID(itemType: String, itemUuid: String): GDriveUploadData { + return gDriveDatabase?.getByUUID(itemType, itemUuid) ?: GDriveUploadData().apply { + this.uuid = itemUuid + this.type = itemType + } + }} + @Dao interface GDriveUploadDataDao { @get:Query("SELECT * FROM gdrive_upload") @@ -64,6 +90,9 @@ interface GDriveUploadDataDao { @Query("SELECT COUNT(*) FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") fun getPendingCount(): Int + @Query("SELECT * FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getAllPending(): List + @Query("SELECT * FROM gdrive_upload WHERE type = :type AND (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") fun getPendingByType(type: String): List } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt index 51d355c8..32ceacd0 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt @@ -15,7 +15,7 @@ fun genGDriveUploadDatabase(context: Context): GDriveUploadDataDao? { return gDriveDatabase } -@Database(entities = [GDriveUploadData::class], version = 2) +@Database(entities = [GDriveUploadData::class], version = 3) abstract class GDriveUploadDatabase : RoomDatabase() { abstract fun drive(): GDriveUploadDataDao } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 437c4e1f..d14a77b1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -2,10 +2,7 @@ package com.bijoysingh.quicknote.drive import android.content.Context import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig -import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadData -import com.bijoysingh.quicknote.database.GDriveUploadDataDao -import com.bijoysingh.quicknote.database.genGDriveUploadDatabase +import com.bijoysingh.quicknote.database.* import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase @@ -25,7 +22,6 @@ import com.maubis.scarlet.base.support.utils.log import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import org.junit.Ignore import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean @@ -89,8 +85,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var tagsSync: GDriveRemoteFolder? = null private var imageSync: GDriveRemoteImageFolder? = null private var syncing = HashMap() - - private var lastSyncState: AtomicBoolean = AtomicBoolean(true) private var syncListener: IPendingUploadListener? = null fun init(helper: GDriveServiceHelper) { @@ -113,6 +107,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { dataType = GDriveDataType.NOTE, database = gDriveDatabase!!, helper = helper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { it }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() @@ -121,6 +116,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { dataType = GDriveDataType.NOTE_META, database = gDriveDatabase!!, helper = helper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() @@ -129,6 +125,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { dataType = GDriveDataType.TAG, database = gDriveDatabase!!, helper = helper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() @@ -137,11 +134,16 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { dataType = GDriveDataType.FOLDER, database = gDriveDatabase!!, helper = helper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() }) - imageSync = GDriveRemoteImageFolder(dataType = GDriveDataType.IMAGE, database = gDriveDatabase!!, helper = helper) + imageSync = GDriveRemoteImageFolder( + dataType = GDriveDataType.IMAGE, + database = gDriveDatabase!!, + helper = helper, + onPendingChange = { verifyAndNotifyPendingStateChange() }) GlobalScope.launch { val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) @@ -265,8 +267,10 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { fun setPendingUploadListener(listener: IPendingUploadListener?) { syncListener = listener - verifyAndNotifyPendingStateChange() - resync() + if (listener !== null) { + verifyAndNotifyPendingStateChange() + resync() + } } private fun verifyAndNotifyPendingStateChange() { @@ -277,10 +281,10 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } val currentPendingState = database.getPendingCount() > 0 - if (currentPendingState != lastSyncState.get()) { - lastSyncState.set(currentPendingState) - syncListener?.onPendingStateUpdate(lastSyncState.get()) - } + + val pending = database.getAllPending().map { "type=${it.type}, uuid=${it.uuid}, fid=${it.fileId}" }.joinToString(separator = "\n") + log("GDrive", "getPendingCount(${ database.getPendingCount()})\n$pending") + syncListener?.onPendingStateUpdate(currentPendingState) } } @@ -382,16 +386,23 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (syncing[type]?.getAndSet(true) == true) { return } - gDriveDatabase?.getPendingByType(type.name)?.forEach { - log("GDrive", "resyncDataSync(${type.name}, ${it.uuid}, ${it.lastUpdateTimestamp}, ${it.gDriveUpdateTimestamp})") - val sameDelete = it.localStateDeleted == it.gDriveStateDeleted - val sameUpdateTime = it.lastUpdateTimestamp == it.gDriveUpdateTimestamp + + val pendingItems = gDriveDatabase?.getPendingByType(type.name) ?: emptyList() + for (pendingItem in pendingItems) { + if (!notifyAttempt(type, pendingItem.uuid)) { + gDriveDatabase?.delete(pendingItem) + continue + } + + log("GDrive", "resyncDataSync(${type.name}, ${pendingItem.uuid}, ${pendingItem.lastUpdateTimestamp}, ${pendingItem.gDriveUpdateTimestamp})") + val sameDelete = pendingItem.localStateDeleted == pendingItem.gDriveStateDeleted + val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.gDriveUpdateTimestamp if (!sameUpdateTime) { when { - sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> insert(type, it) - sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteInsert(type, it) - !sameDelete && it.lastUpdateTimestamp > it.gDriveUpdateTimestamp -> remove(type, it) - !sameDelete && it.lastUpdateTimestamp < it.gDriveUpdateTimestamp -> onRemoteRemove(type, it) + sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> insert(type, pendingItem) + sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteInsert(type, pendingItem) + !sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> remove(type, pendingItem) + !sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteRemove(type, pendingItem) } } } @@ -402,6 +413,27 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { * Update the database about information */ + /** + * Notifies that an attempt to update this item was made. + * If this number is over 10, we will delete the item to prevent issues + * + * @return if false, the item will be deleted from the database + */ + private fun notifyAttempt(itemType: GDriveDataType, itemUUID: String): Boolean { + val database = gDriveDatabase + if (database === null) { + return false + } + + val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + existing.apply { + attempts += 1 + save(database) + } + + return existing.attempts < 10 + } + private fun localDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, removed: Boolean = false) { GlobalScope.launch { val database = gDriveDatabase @@ -410,15 +442,13 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } log("GDrive", "localDatabaseUpdate(${itemType.name}, $itemUUID)") - val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() + val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { - uuid = itemUUID - type = itemType.name + attempts = 0 lastUpdateTimestamp = Math.max(gDriveUpdateTimestamp + 1, getTrueCurrentTime()) localStateDeleted = removed save(database) } - verifyAndNotifyPendingStateChange() } } @@ -431,15 +461,12 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") - val existing = database.getByUUID(itemType.name, itemUUID) ?: GDriveUploadData() + val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { - uuid = itemUUID - type = itemType.name lastUpdateTimestamp = gDriveUpdateTimestamp localStateDeleted = gDriveStateDeleted save(database) } - verifyAndNotifyPendingStateChange() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index c85f30fd..8920b1cc 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.maubis.scarlet.base.support.utils.log import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -12,8 +13,9 @@ class GDriveRemoteFolder( dataType: GDriveDataType, database: GDriveUploadDataDao, helper: GDriveServiceHelper, + onPendingChange: () -> Unit, val serialiser: (T) -> String, - val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper) { + val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { var contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -25,6 +27,8 @@ class GDriveRemoteFolder( var deletedPendingActions = emptySet().toMutableSet() val deletedFiles = emptyMap().toMutableMap() + val duplicateFilesToDelete: MutableList = emptyList().toMutableList() + fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { GlobalScope.launch(Dispatchers.IO) { contentLoading.set(true) @@ -33,13 +37,18 @@ class GDriveRemoteFolder( val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() files.forEach { file -> - localFileIds[file.name] = file.id - notifyDriveData(file) + if (localFileIds.containsKey(file.name)) { + duplicateFilesToDelete.add(file.id) + } else { + localFileIds[file.name] = file.id + notifyDriveData(file) + } } contentFiles.clear() contentFiles.putAll(localFileIds) contentLoading.set(false) + GlobalScope.launch { executeAllDuplicateDeletion() } GlobalScope.launch { executeInsertPendingActions() } GlobalScope.launch { onLoaded() } } @@ -54,19 +63,33 @@ class GDriveRemoteFolder( val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() files.forEach { file -> - localFileIds[file.name] = file.id - notifyDriveData(file, true) + if (localFileIds.containsKey(file.name)) { + duplicateFilesToDelete.add(file.id) + } else { + localFileIds[file.name] = file.id + notifyDriveData(file, true) + } } deletedFiles.clear() deletedFiles.putAll(localFileIds) deletedLoading.set(false) + GlobalScope.launch { executeAllDuplicateDeletion() } GlobalScope.launch { executeDeletePendingActions() } GlobalScope.launch { onLoaded() } } } } + fun executeAllDuplicateDeletion() { + val files = ArrayList() + files.addAll(duplicateFilesToDelete) + duplicateFilesToDelete.clear() + files.forEach { fileId -> + helper.removeFileOrFolder(fileId) + } + } + fun executeInsertPendingActions() { contentPendingActions.forEach { uuid -> GlobalScope.launch { @@ -96,8 +119,10 @@ class GDriveRemoteFolder( try { val data = serialiser(item) val fileId = contentFiles[uuid] - val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp - ?: getTrueCurrentTime() + val existing = database.getByUUID(dataType.name, uuid) + val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() + + log("GDriveFolder", "uuid=$uuid efid=${existing?.fileId} ets=${existing?.lastUpdateTimestamp} :: fid=$fileId, ts=$timestamp") if (fileId !== null) { helper.saveFile(fileId, uuid, data, timestamp).addOnCompleteListener { val file = it.result @@ -130,20 +155,22 @@ class GDriveRemoteFolder( val existingFileUid = contentFiles[uuid] if (existingFileUid !== null) { - helper.removeFileOrFolder(existingFileUid) - contentFiles.remove(uuid) - } + helper.removeFileOrFolder(existingFileUid).addOnCompleteListener { + contentFiles.remove(uuid) + if (deletedFolderUid == INVALID_FILE_ID) { + return@addOnCompleteListener + } - if (deletedFolderUid == INVALID_FILE_ID) { - return - } - val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp - ?: getTrueCurrentTime() - helper.createFileWithData(deletedFolderUid, uuid, "", timestamp).addOnCompleteListener { - val file = it.result - if (file !== null) { - deletedFiles[uuid] = file.id - notifyDriveData(file.id, uuid, timestamp, true) + GlobalScope.launch { + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() + helper.createFileWithData(deletedFolderUid, uuid, uuid, timestamp).addOnCompleteListener { + val file = it.result + if (file !== null) { + deletedFiles[uuid] = file.id + notifyDriveData(file.id, uuid, timestamp, true) + } + } + } } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 7218b9e7..cc277d6c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveDatabaseHelper import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.google.api.services.drive.model.File @@ -10,7 +11,8 @@ import kotlinx.coroutines.launch abstract class GDriveRemoteFolderBase( val dataType: GDriveDataType, val database: GDriveUploadDataDao, - val helper: GDriveServiceHelper) { + val helper: GDriveServiceHelper, + val onPendingChange: () -> Unit) { protected fun notifyDriveData(file: File, deleted: Boolean = false) { val modifiedTime = file.modifiedTime?.value ?: 0L @@ -19,11 +21,9 @@ abstract class GDriveRemoteFolderBase( protected fun notifyDriveData(uid: String, name: String, modifiedTime: Long, deleted: Boolean = false) { GlobalScope.launch { - val uploadData = database.getByUUID(dataType.name, name) - if (uploadData == null) { - GDriveUploadData().apply { - uuid = name - type = dataType.name + val uploadData = GDriveDatabaseHelper.getByUUID(dataType, name) + if (uploadData.unsaved()) { + uploadData.apply { fileId = uid gDriveUpdateTimestamp = modifiedTime gDriveStateDeleted = deleted diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 5dd35e1d..479b47b7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -40,7 +40,8 @@ fun toImageUUID(imageUuid: String): ImageUUID? { class GDriveRemoteImageFolder( dataType: GDriveDataType, database: GDriveUploadDataDao, - helper: GDriveServiceHelper) : GDriveRemoteFolderBase(dataType, database, helper) { + helper: GDriveServiceHelper, + onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { val contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index bf7fa678..ea5315eb 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -1,8 +1,6 @@ package com.bijoysingh.quicknote.drive import android.os.SystemClock -import android.util.Log -import com.bijoysingh.quicknote.BuildConfig import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.api.client.http.ByteArrayContent @@ -18,7 +16,6 @@ import java.io.BufferedReader import java.io.FileOutputStream import java.io.InputStreamReader import java.io.InterruptedIOException -import java.net.SocketTimeoutException import java.util.* import java.util.concurrent.Callable import java.util.concurrent.Executors @@ -31,14 +28,14 @@ const val GOOGLE_DRIVE_FILE_MIME_TYPE = "text/plain" const val GOOGLE_DRIVE_IMAGE_MIME_TYPE = "image/jpeg" const val INVALID_FILE_ID = "__invalid__" -const val MAX_THRESHOLD_QUERIES_PER_SECOND = 2 +const val MAX_THRESHOLD_QUERIES_PER_SECOND = 4 const val MIN_RESET_QUERIES_PER_SECOND = 0.1 var lastCheckpointTime: AtomicLong = AtomicLong(0L) var numQueriesSinceLastCheckpoint: AtomicLong = AtomicLong(0L) class ErrorCallable(val action: String, val callable: Callable) : Callable { - private var delay: Long = 100L + private var delay: Long = 200L override fun call(): T? { val lastCheckpoint = lastCheckpointTime.get() @@ -49,12 +46,11 @@ class ErrorCallable(val action: String, val callable: Callable) : Callable val currentCount = numQueriesSinceLastCheckpoint.get() * 1.0 val deltaTimeS = (System.currentTimeMillis() - lastCheckpointTime.get()) / 1000.0 val currentQueriesPerSecond = (currentCount / deltaTimeS) - log("GDrive", "Request being called: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=$currentQueriesPerSecond") + // log("GDrive", "Request being called: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS, requestRate=$currentQueriesPerSecond") if (currentCount >= 10 && deltaTimeS > 0) { when { currentQueriesPerSecond > MAX_THRESHOLD_QUERIES_PER_SECOND -> { - log("GDrive", "Rate limiting measures taken: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS") - delay *= 2 + // log("GDrive", "Rate limiting measures taken: action=$action, currentCount=$currentCount, deltaTimeS=$deltaTimeS, delay=$delay") SystemClock.sleep(delay) return call() } @@ -178,6 +174,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { .setPageSize(1000) .setFields("files(name, id, modifiedTime, mimeType)") .setQ("mimeType = '$mimeType' and '$parentUid' in parents") + .setOrderBy("modifiedTime desc") .execute() }) } @@ -213,7 +210,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun removeFileOrFolder(fileUid: String): Task { log("GDrive", "removeFileOrFolder($fileUid)") return execute("removeFileOrFolder", Callable { - mDriveService.files().delete(fileUid) + mDriveService.files().delete(fileUid).execute() null }) } From 8e80b65d4fcd7296fef254f9f98c73dc36871fa4 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 4 Jun 2019 00:03:50 +0100 Subject: [PATCH 037/134] [GDrive] Improving failed synchronisation cases --- .../com/maubis/scarlet/base/MainActivity.kt | 29 +++-- .../base/config/auth/IAuthenticator.kt | 2 + .../config/auth/IPendingUploadListener.kt | 5 + .../base/config/auth/NullAuthenticator.kt | 2 + .../main/specs/MainActivityBottomBarSpec.kt | 61 +++++++---- base/src/main/res/layout/activity_main.xml | 7 ++ base/src/main/res/values/strings.xml | 1 + .../quicknote/drive/GDriveRemoteDatabase.kt | 101 ++++++++++++++---- .../quicknote/drive/GDriveRemoteFolder.kt | 101 +++++++++++------- .../quicknote/drive/GDriveRemoteFolderBase.kt | 7 +- .../drive/GDriveRemoteImageFolder.kt | 39 +++++-- .../quicknote/scarlet/ScarletAuthenticator.kt | 6 ++ 12 files changed, 260 insertions(+), 101 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index c64bdd76..b46d5116 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -76,6 +76,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { private lateinit var tagAndColorPicker: TagsAndColorPickerViewHolder private var lastSyncState: AtomicBoolean = AtomicBoolean(false) + private var lastSyncHappening: AtomicBoolean = AtomicBoolean(false) var config: SearchConfig = SearchConfig(mode = HomeNavigationState.DEFAULT) var isInSearchMode: Boolean = false @@ -329,30 +330,37 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { .build())) } - fun notifySyncingInformation(isSyncPending: Boolean) { + fun notifySyncingInformation(isSyncHappening: Boolean, isSyncPending: Boolean) { val componentContext = ComponentContext(this) if (!instance.authenticator().isLoggedIn(this) || instance.authenticator().isLegacyLoggedIn()) { return } - if (lastSyncState.getAndSet(isSyncPending) == isSyncPending) { + if (lastSyncState.getAndSet(isSyncPending) == isSyncPending + && lastSyncHappening.getAndSet(isSyncHappening) == isSyncHappening) { return } - if (!isSyncPending) { + if (!isSyncPending && !isSyncHappening) { GlobalScope.launch(Dispatchers.Main) { - lithoPreBottomToolbar.removeAllViews() + lithoSyncingBottomToolbar.removeAllViews() } return } GlobalScope.launch(Dispatchers.Main) { - lithoPreBottomToolbar.removeAllViews() - lithoPreBottomToolbar.addView(LithoView.create(componentContext, + lithoSyncingBottomToolbar.removeAllViews() + lithoSyncingBottomToolbar.addView(LithoView.create(componentContext, MainActivitySyncingNow.create(componentContext) - .onClick {} + .isSyncHappening(isSyncHappening) + .onClick { + instance.authenticator().requestSync() + } .build())) + if (!isSyncHappening && isSyncPending) { + instance.authenticator().requestSync() + } } } @@ -378,10 +386,15 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { notifyDisabledSync() instance.authenticator().setPendingUploadListener(object : IPendingUploadListener { + override fun onPendingSyncsUpdate(isSyncHappening: Boolean) { + notifySyncingInformation(isSyncHappening, lastSyncState.get()) + } + override fun onPendingStateUpdate(isDataSyncPending: Boolean) { - notifySyncingInformation(isDataSyncPending) + notifySyncingInformation(lastSyncHappening.get(), isDataSyncPending) } }) + instance.authenticator().requestSync() } fun resetAndSetupData() { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index c86e2b47..a38a059a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -22,5 +22,7 @@ interface IAuthenticator { fun setPendingUploadListener(listener: IPendingUploadListener?) + fun requestSync() + fun logout() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt index fcafd3a7..365512fc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IPendingUploadListener.kt @@ -5,4 +5,9 @@ interface IPendingUploadListener { * Fires when the pending state changes. */ fun onPendingStateUpdate(isDataSyncPending: Boolean) + + /** + * Pending Sync Count state change + */ + fun onPendingSyncsUpdate(isSyncHappening: Boolean) } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index d31b9c78..48dffc15 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -22,4 +22,6 @@ class NullAuthenticator : IAuthenticator { override fun isLegacyLoggedIn(): Boolean = false override fun setPendingUploadListener(listener: IPendingUploadListener?) {} + + override fun requestSync() {} } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index ec4ecf6b..43d22fbb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -7,6 +7,7 @@ import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent import com.facebook.litho.annotations.Prop +import com.facebook.litho.widget.Image import com.facebook.litho.widget.Progress import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign @@ -23,10 +24,7 @@ import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.folder.sheet.CreateOrEditFolderBottomSheet import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.specs.EmptySpec -import com.maubis.scarlet.base.support.specs.ToolbarColorConfig -import com.maubis.scarlet.base.support.specs.bottomBarCard -import com.maubis.scarlet.base.support.specs.bottomBarRoundIcon +import com.maubis.scarlet.base.support.specs.* import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.support.ui.ThemeColorType import kotlinx.coroutines.Dispatchers @@ -174,33 +172,50 @@ object MainActivityDisabledSyncSpec { @LayoutSpec object MainActivitySyncingNowSpec { @OnCreateLayout - fun onCreate(context: ComponentContext): Component { + fun onCreate(context: ComponentContext, @Prop isSyncHappening: Boolean): Component { val colorConfig = ToolbarColorConfig( toolbarBackgroundColor = instance.themeController().get(ThemeColorType.TOOLBAR_BACKGROUND), toolbarIconColor = instance.themeController().get(ThemeColorType.TOOLBAR_ICON) ) + val syncText = when (isSyncHappening) { + true -> R.string.home_syncing_top_layout + false -> R.string.home_pending_backup_top_layout + } + val syncIcon = when (isSyncHappening) { + true -> Progress.create(context) + .widthDip(24f) + .alpha(0.8f) + .marginDip(YogaEdge.END, 8f) + .color(colorConfig.toolbarIconColor) + false -> Image.create(context) + .heightDip(24f) + .widthDip(24f) + .marginDip(YogaEdge.END, 8f) + .alpha(0.8f) + .drawableRes(R.drawable.icon_folder_sync) + } + val row = Row.create(context) .widthPercent(100f) .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 4f) - .child(Row.create(context) - .child(EmptySpec.create(context).flexGrow(1f)) - .child( - Progress.create(context) - .widthDip(48f) - .paddingDip(YogaEdge.ALL, 8f) - .marginDip(YogaEdge.HORIZONTAL, 8f) - .alpha(0.6f) - .color(colorConfig.toolbarIconColor)) - .flexGrow(1f)) - .child(Text.create(context) - .typeface(FONT_MONSERRAT) - .textRes(R.string.home_syncing_top_layout) - .textSizeRes(R.dimen.font_size_normal) - .textColor(colorConfig.toolbarIconColor)) + .paddingDip(YogaEdge.HORIZONTAL, 8f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .alpha(0.8f) .child(EmptySpec.create(context).flexGrow(1f)) - .clickHandler(MainActivityDisabledSync.onClickEvent(context)) - return bottomBarCard(context, row.build(), colorConfig).build() + .child(Row.create(context) + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .backgroundRes(R.drawable.login_button_disabled) + .child(syncIcon) + .child(Text.create(context) + .typeface(FONT_MONSERRAT) + .textRes(syncText) + .textSizeRes(R.dimen.font_size_normal) + .textColor(colorConfig.toolbarIconColor))) + .clickHandler(MainActivitySyncingNow.onClickEvent(context)) + return row.build() } @OnEvent(ClickEvent::class) diff --git a/base/src/main/res/layout/activity_main.xml b/base/src/main/res/layout/activity_main.xml index 21eef020..940a7e69 100644 --- a/base/src/main/res/layout/activity_main.xml +++ b/base/src/main/res/layout/activity_main.xml @@ -42,6 +42,13 @@ android:layout_height="wrap_content" /> + + Find help on how to use the features in the app Syncing + Pending Backup Developer Options diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index d14a77b1..f0fa8ee1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger const val FOLDER_NAME_IMAGES = "images" const val FOLDER_NAME_NOTES = "notes" @@ -86,6 +87,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var imageSync: GDriveRemoteImageFolder? = null private var syncing = HashMap() private var syncListener: IPendingUploadListener? = null + private var pendingSyncs: AtomicInteger = AtomicInteger(0) fun init(helper: GDriveServiceHelper) { val context = weakContext.get() @@ -108,6 +110,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, + onPendingSyncComplete = { decrementPendingSyncs() }, serialiser = { it }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() @@ -117,6 +120,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, + onPendingSyncComplete = { decrementPendingSyncs() }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() @@ -126,6 +130,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, + onPendingSyncComplete = { decrementPendingSyncs() }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() @@ -135,6 +140,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, + onPendingSyncComplete = { decrementPendingSyncs() }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() @@ -143,7 +149,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { dataType = GDriveDataType.IMAGE, database = gDriveDatabase!!, helper = helper, - onPendingChange = { verifyAndNotifyPendingStateChange() }) + onPendingChange = { verifyAndNotifyPendingStateChange() }, + onPendingSyncComplete = { decrementPendingSyncs() }) GlobalScope.launch { val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) @@ -185,45 +192,60 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { * Initialisation Methods */ private fun initSubRootFolder(folderName: String, folderId: String) { + incrementPendingSyncs() when (folderName) { FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNote) { GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) } sGDriveFirstSyncNote = true } + decrementPendingSyncs() } - FOLDER_NAME_NOTES_META -> notesMetaSync?.initContentFolderId(folderId) { - if (!sGDriveFirstSyncNoteMeta) { - GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } - sGDriveFirstSyncNoteMeta = true + FOLDER_NAME_NOTES_META -> { + notesMetaSync?.initContentFolderId(folderId) { + if (!sGDriveFirstSyncNoteMeta) { + GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } + sGDriveFirstSyncNoteMeta = true + } + decrementPendingSyncs() } + notesMetaSync?.initDeletedFolderId(INVALID_FILE_ID) {} } FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncTag) { GlobalScope.launch { resyncDataSync(GDriveDataType.TAG) } sGDriveFirstSyncTag = true } + decrementPendingSyncs() } FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncFolder) { GlobalScope.launch { resyncDataSync(GDriveDataType.FOLDER) } sGDriveFirstSyncFolder = true } + decrementPendingSyncs() } FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncImage) { GlobalScope.launch { resyncDataSync(GDriveDataType.IMAGE) } sGDriveFirstSyncImage = true } + decrementPendingSyncs() + } + FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) { + decrementPendingSyncs() + } + FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) { + decrementPendingSyncs() + } + FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) { + decrementPendingSyncs() } - FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) {} - FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) {} - FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) {} } } private fun onRootFolderLoaded(rootFolderId: String) { - createFolders(rootFolderId, listOf(FOLDER_NAME_IMAGES, FOLDER_NAME_NOTES, FOLDER_NAME_TAGS, FOLDER_NAME_FOLDERS, FOLDER_NAME_NOTES_META)) + createFolders(rootFolderId, listOf(FOLDER_NAME_NOTES, FOLDER_NAME_NOTES_META, FOLDER_NAME_FOLDERS, FOLDER_NAME_TAGS, FOLDER_NAME_IMAGES)) createFolders(rootFolderId, listOf(FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS)) } @@ -269,7 +291,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { syncListener = listener if (listener !== null) { verifyAndNotifyPendingStateChange() - resync() } } @@ -283,11 +304,27 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { val currentPendingState = database.getPendingCount() > 0 val pending = database.getAllPending().map { "type=${it.type}, uuid=${it.uuid}, fid=${it.fileId}" }.joinToString(separator = "\n") - log("GDrive", "getPendingCount(${ database.getPendingCount()})\n$pending") + log("GDrive", "getPendingCount(${database.getPendingCount()})\n$pending") syncListener?.onPendingStateUpdate(currentPendingState) } } + @Synchronized + private fun decrementPendingSyncs() { + pendingSyncs.decrementAndGet() + if (pendingSyncs.get() <= 0) { + pendingSyncs.set(0) + syncListener?.onPendingSyncsUpdate(false) + } + } + + @Synchronized + private fun incrementPendingSyncs() { + pendingSyncs.incrementAndGet() + if (pendingSyncs.get() >= 1) { + syncListener?.onPendingSyncsUpdate(true) + } + } /** * Notify local changes to the notes */ @@ -387,22 +424,28 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } - val pendingItems = gDriveDatabase?.getPendingByType(type.name) ?: emptyList() + val pendingItems = gDriveDatabase?.getPendingByType(type.name) ?: emptyList() for (pendingItem in pendingItems) { if (!notifyAttempt(type, pendingItem.uuid)) { - gDriveDatabase?.delete(pendingItem) + // Think of a better solution here... + // gDriveDatabase?.delete(pendingItem) + remoteDatabaseUpdate(type, pendingItem.uuid) continue } log("GDrive", "resyncDataSync(${type.name}, ${pendingItem.uuid}, ${pendingItem.lastUpdateTimestamp}, ${pendingItem.gDriveUpdateTimestamp})") val sameDelete = pendingItem.localStateDeleted == pendingItem.gDriveStateDeleted + val localDeleted = pendingItem.localStateDeleted val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.gDriveUpdateTimestamp - if (!sameUpdateTime) { + if (!sameUpdateTime || !sameUpdateTime) { when { + sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp && localDeleted -> remove(type, pendingItem) sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> insert(type, pendingItem) + sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp && localDeleted -> onRemoteRemove(type, pendingItem) sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteInsert(type, pendingItem) !sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> remove(type, pendingItem) !sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteRemove(type, pendingItem) + !sameDelete && sameUpdateTime -> remoteDatabaseUpdate(type, pendingItem.uuid) // Ignoring } } } @@ -431,7 +474,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { save(database) } - return existing.attempts < 10 + return existing.attempts < 16 } private fun localDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, removed: Boolean = false) { @@ -463,6 +506,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { + attempts = 0 lastUpdateTimestamp = gDriveUpdateTimestamp localStateDeleted = gDriveStateDeleted save(database) @@ -480,31 +524,33 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } + log("GDriveRemote", "insert(${type.name}, ${data.uuid})") + incrementPendingSyncs() when (type) { GDriveDataType.NOTE -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { notesSync?.insert(data.uuid, this) - } + } ?: decrementPendingSyncs() } GDriveDataType.NOTE_META -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { notesMetaSync?.insert(data.uuid, this) - } + } ?: decrementPendingSyncs() } GDriveDataType.TAG -> { ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { tagsSync?.insert(this.uuid, this) - } + } ?: decrementPendingSyncs() } GDriveDataType.FOLDER -> { ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { foldersSync?.insert(this.uuid, this) - } + } ?: decrementPendingSyncs() } GDriveDataType.IMAGE -> { toImageUUID(data.uuid)?.apply { imageSync?.insert(this) - } + } ?: decrementPendingSyncs() } } } @@ -514,6 +560,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } + log("GDriveRemote", "remove(${type.name}, ${data.uuid})") + incrementPendingSyncs() val uuid = data.uuid when (type) { GDriveDataType.NOTE -> notesSync?.delete(uuid) @@ -522,8 +570,9 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { GDriveDataType.FOLDER -> foldersSync?.delete(uuid) GDriveDataType.IMAGE -> { val imageUUID = toImageUUID(uuid) - if (imageUUID !== null) { - imageSync?.delete(imageUUID) + when { + imageUUID !== null -> imageSync?.delete(imageUUID) + else -> decrementPendingSyncs() } } } @@ -539,6 +588,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } + log("GDriveRemote", "onRemoteInsert(${type.name}, ${data.uuid})") + incrementPendingSyncs() when (type) { GDriveDataType.NOTE -> { onRemoteInsertImpl(data.fileId) { @@ -603,6 +654,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) if (imageFile.exists()) { remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + decrementPendingSyncs() return } @@ -610,6 +662,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (it.result == true) { remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) } + decrementPendingSyncs() } } } @@ -626,6 +679,8 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { return } + log("GDriveRemote", "onRemoteRemove(${type.name}, ${data.uuid})") + incrementPendingSyncs() when (type) { GDriveDataType.NOTE -> { IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) @@ -644,6 +699,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } remoteDatabaseUpdate(type, data.uuid) + decrementPendingSyncs() } /** @@ -656,6 +712,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (data !== null) { onDataAvailable(data) } + decrementPendingSyncs() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 8920b1cc..f036e35a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -3,7 +3,6 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadDataDao import com.maubis.scarlet.base.support.utils.log -import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -14,8 +13,9 @@ class GDriveRemoteFolder( database: GDriveUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit, + onPendingSyncComplete: () -> Unit, val serialiser: (T) -> String, - val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { + val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { var contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -56,6 +56,13 @@ class GDriveRemoteFolder( } fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { + if (fUid == INVALID_FILE_ID) { + deletedLoading.set(false) + GlobalScope.launch { executeDeletePendingActions() } + GlobalScope.launch { onLoaded() } + return + } + GlobalScope.launch(Dispatchers.IO) { deletedLoading.set(true) deletedFolderUid = fUid @@ -116,32 +123,35 @@ class GDriveRemoteFolder( return } - try { - val data = serialiser(item) - val fileId = contentFiles[uuid] - val existing = database.getByUUID(dataType.name, uuid) - val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() + val data = serialiser(item) + val fileId = contentFiles[uuid] + val existing = database.getByUUID(dataType.name, uuid) + val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() - log("GDriveFolder", "uuid=$uuid efid=${existing?.fileId} ets=${existing?.lastUpdateTimestamp} :: fid=$fileId, ts=$timestamp") - if (fileId !== null) { - helper.saveFile(fileId, uuid, data, timestamp).addOnCompleteListener { + log("GDriveFolder", "uuid=$uuid efid=${existing?.fileId} ets=${existing?.lastUpdateTimestamp} :: fid=$fileId, ts=$timestamp") + if (fileId !== null) { + helper.saveFile(fileId, uuid, data, timestamp) + .addOnCompleteListener { + val file = it.result + if (file !== null) { + notifyDriveData(file.id, uuid, timestamp) + } + onPendingSyncComplete() + } + .addOnCanceledListener { onPendingSyncComplete() } + return + } + helper.createFileWithData(contentFolderUid, uuid, data, timestamp) + .addOnCompleteListener { val file = it.result if (file !== null) { + contentFiles[uuid] = file.id notifyDriveData(file.id, uuid, timestamp) } + onPendingSyncComplete() } - return - } - helper.createFileWithData(contentFolderUid, uuid, data, timestamp).addOnCompleteListener { - val file = it.result - if (file !== null) { - contentFiles[uuid] = file.id - notifyDriveData(file.id, uuid, timestamp) - } - } - } catch (exception: Exception) { - maybeThrow(exception) - } + .addOnCanceledListener { onPendingSyncComplete() } + } /** @@ -154,24 +164,41 @@ class GDriveRemoteFolder( } val existingFileUid = contentFiles[uuid] - if (existingFileUid !== null) { - helper.removeFileOrFolder(existingFileUid).addOnCompleteListener { - contentFiles.remove(uuid) - if (deletedFolderUid == INVALID_FILE_ID) { - return@addOnCompleteListener + if (existingFileUid === null) { + GlobalScope.launch { + val existing = database.getByUUID(dataType.name, uuid) + if (existing !== null) { + database.delete(existing) + onPendingChange() } + onPendingSyncComplete() + } + return + } - GlobalScope.launch { - val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp ?: getTrueCurrentTime() - helper.createFileWithData(deletedFolderUid, uuid, uuid, timestamp).addOnCompleteListener { - val file = it.result - if (file !== null) { - deletedFiles[uuid] = file.id - notifyDriveData(file.id, uuid, timestamp, true) - } + helper.removeFileOrFolder(existingFileUid) + .addOnCompleteListener { + contentFiles.remove(uuid) + if (deletedFolderUid == INVALID_FILE_ID) { + onPendingSyncComplete() + return@addOnCompleteListener + } + + GlobalScope.launch { + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp + ?: getTrueCurrentTime() + helper.createFileWithData(deletedFolderUid, uuid, uuid, timestamp) + .addOnCompleteListener { + val file = it.result + if (file !== null) { + deletedFiles[uuid] = file.id + notifyDriveData(file.id, uuid, timestamp, true) + } + onPendingSyncComplete() + } + .addOnCanceledListener { onPendingSyncComplete() } } } - } - } + .addOnCanceledListener { onPendingSyncComplete() } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index cc277d6c..66fbdcf4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -12,7 +12,8 @@ abstract class GDriveRemoteFolderBase( val dataType: GDriveDataType, val database: GDriveUploadDataDao, val helper: GDriveServiceHelper, - val onPendingChange: () -> Unit) { + val onPendingChange: () -> Unit, + val onPendingSyncComplete: () -> Unit) { protected fun notifyDriveData(file: File, deleted: Boolean = false) { val modifiedTime = file.modifiedTime?.value ?: 0L @@ -29,6 +30,7 @@ abstract class GDriveRemoteFolderBase( gDriveStateDeleted = deleted save(database) } + onPendingChange() return@launch } @@ -39,6 +41,7 @@ abstract class GDriveRemoteFolderBase( gDriveStateDeleted = deleted save(database) } + onPendingChange() return@launch } @@ -52,6 +55,7 @@ abstract class GDriveRemoteFolderBase( gDriveStateDeleted = deleted save(database) } + onPendingChange() } if (uploadData.fileId == uid && uploadData.gDriveUpdateTimestamp != modifiedTime) { @@ -61,6 +65,7 @@ abstract class GDriveRemoteFolderBase( gDriveStateDeleted = deleted save(database) } + onPendingChange() } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 479b47b7..821721d8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -41,7 +41,8 @@ class GDriveRemoteImageFolder( dataType: GDriveDataType, database: GDriveUploadDataDao, helper: GDriveServiceHelper, - onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { + onPendingChange: () -> Unit, + onPendingSyncComplete: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { val contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -77,19 +78,32 @@ class GDriveRemoteImageFolder( } if (contentFiles.containsKey(id)) { + GlobalScope.launch { + database.getByUUID(dataType.name, id.name())?.apply { + gDriveUpdateTimestamp = lastUpdateTimestamp + gDriveStateDeleted = localStateDeleted + save(database) + } + onPendingChange() + onPendingSyncComplete() + } return } val gDriveUUID = id.name() - val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() + val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp + ?: getTrueCurrentTime() val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) - helper.createFileWithData(contentFolderUid, gDriveUUID, imageFile, timestamp).addOnCompleteListener { - val file = it.result - if (file !== null) { - contentFiles[id] = file.id - notifyDriveData(file.id, gDriveUUID, timestamp) - } - } + helper.createFileWithData(contentFolderUid, gDriveUUID, imageFile, timestamp) + .addOnCompleteListener { + val file = it.result + if (file !== null) { + contentFiles[id] = file.id + notifyDriveData(file.id, gDriveUUID, timestamp) + } + onPendingSyncComplete() + } + .addOnCanceledListener { onPendingSyncComplete() } } fun delete(id: ImageUUID) { @@ -99,10 +113,15 @@ class GDriveRemoteImageFolder( } if (!contentFiles.containsKey(id)) { + onPendingSyncComplete() return } helper.removeFileOrFolder(contentFiles[id] ?: INVALID_FILE_ID) - contentFiles.remove(id) + .addOnCompleteListener { + contentFiles.remove(id) + onPendingSyncComplete() + } + .addOnCanceledListener { onPendingSyncComplete() } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index b1b96a9c..7ad44d65 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -67,6 +67,12 @@ class ScarletAuthenticator() : IAuthenticator { } } + override fun requestSync() { + if (sGDriveLoggedIn) { + gDrive?.resync() + } + } + override fun openLoginActivity(context: Context) = Runnable { context.startActivity(Intent(context, GDriveLoginActivity::class.java)) } From b572a3ae45a550f758d9b1cf7e465c31e076fd8c Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 15 Jun 2019 20:43:44 +0100 Subject: [PATCH 038/134] [GDrive] Fixing Syncing between logins, logouts --- .../maubis/scarlet/base/config/CoreConfig.kt | 3 + .../scarlet/base/config/MaterialNoteConfig.kt | 8 + .../database/remote/IRemoteDatabaseState.kt | 6 + .../service/SyncedNoteBroadcastReceiver.kt | 4 + .../base/support/database/MigrationUtils.kt | 18 ++ .../java/com/bijoysingh/quicknote/Scarlet.kt | 2 + .../quicknote/drive/GDriveAuthenticator.kt | 2 + .../quicknote/drive/GDriveLoginActivity.kt | 18 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 210 ++++-------------- .../drive/GDriveRemoteDatabaseState.kt | 162 ++++++++++++++ .../quicknote/drive/GDriveRemoteFolderBase.kt | 2 +- .../drive/GDriveRemoteImageFolder.kt | 27 ++- .../quicknote/drive/GDriveServiceHelper.kt | 11 +- .../quicknote/scarlet/ScarletConfig.kt | 4 + .../quicknote/scarlet/ScarletFolderActor.kt | 9 +- .../quicknote/scarlet/ScarletNoteActor.kt | 10 +- .../quicknote/scarlet/ScarletTagActor.kt | 10 +- 17 files changed, 303 insertions(+), 203 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseState.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 4cc24d81..3d6c465d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -17,6 +17,7 @@ import com.maubis.scarlet.base.core.tag.ITagActor import com.maubis.scarlet.base.database.FoldersProvider import com.maubis.scarlet.base.database.NotesProvider import com.maubis.scarlet.base.database.TagsProvider +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note @@ -57,6 +58,8 @@ abstract class CoreConfig(context: Context) { abstract fun remoteConfigFetcher(): IRemoteConfigFetcher + abstract fun remoteDatabaseState(): IRemoteDatabaseState + abstract fun startListener(activity: AppCompatActivity) abstract fun appFlavor(): Flavor diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 4ea9b0ab..5a5ca6a5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -17,6 +17,7 @@ import com.maubis.scarlet.base.core.tag.MaterialTagActor import com.maubis.scarlet.base.database.FoldersProvider import com.maubis.scarlet.base.database.NotesProvider import com.maubis.scarlet.base.database.TagsProvider +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note @@ -59,6 +60,13 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun remoteConfigFetcher(): IRemoteConfigFetcher = NullRemoteConfigFetcher() + override fun remoteDatabaseState(): IRemoteDatabaseState { + return object: IRemoteDatabaseState { + override fun notifyInsert(data: Any, onExecution: () -> Unit) {} + override fun notifyRemove(data: Any, onExecution: () -> Unit) {} + } + } + override fun startListener(activity: AppCompatActivity) {} override fun appFlavor(): Flavor = Flavor.NONE diff --git a/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseState.kt b/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseState.kt new file mode 100644 index 00000000..4b66085b --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseState.kt @@ -0,0 +1,6 @@ +package com.maubis.scarlet.base.database.remote + +interface IRemoteDatabaseState { + fun notifyInsert(data: Any, onExecution: () -> Unit) + fun notifyRemove(data: Any, onExecution: () -> Unit) +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt b/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt index 5f9eb21d..48243199 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt @@ -23,6 +23,8 @@ fun getNoteIntentFilter(): IntentFilter { filter.addAction(NoteBroadcast.NOTE_DELETED.name) filter.addAction(NoteBroadcast.TAG_CHANGED.name) filter.addAction(NoteBroadcast.TAG_DELETED.name) + filter.addAction(NoteBroadcast.FOLDER_CHANGED.name) + filter.addAction(NoteBroadcast.FOLDER_DELETED.name) return filter } @@ -53,6 +55,8 @@ class SyncedNoteBroadcastReceiver(val listener: () -> Unit) : BroadcastReceiver( NoteBroadcast.NOTE_DELETED.name -> listener() NoteBroadcast.TAG_CHANGED.name -> listener() NoteBroadcast.TAG_DELETED.name -> listener() + NoteBroadcast.FOLDER_CHANGED.name -> listener() + NoteBroadcast.FOLDER_DELETED.name -> listener() } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 0b032f67..6dd4ea53 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -15,6 +15,8 @@ import com.maubis.scarlet.base.support.ui.KEY_NIGHT_THEME import com.maubis.scarlet.base.support.ui.Theme import com.maubis.scarlet.base.support.utils.getLastUsedAppVersionCode import com.maubis.scarlet.base.support.utils.maybeThrow +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import java.io.File import java.util.* @@ -22,6 +24,7 @@ const val KEY_MIGRATE_THEME = "KEY_MIGRATE_THEME" const val KEY_MIGRATE_DEFAULT_VALUES = "KEY_MIGRATE_DEFAULT_VALUES" const val KEY_MIGRATE_REMINDERS = "KEY_MIGRATE_REMINDERS" const val KEY_MIGRATE_IMAGES = "KEY_MIGRATE_IMAGES" +const val KEY_MIGRATE_TO_GDRIVE_DATABASE = "KEY_MIGRATE_TO_GDRIVE_DATABASE" class Migrator(val context: Context) { @@ -63,6 +66,21 @@ class Migrator(val context: Context) { ApplicationBase.instance.store().put(KEY_APP_THEME, Theme.DARK.name) ApplicationBase.instance.store().put(KEY_LIST_VIEW, true) } + + runTask(KEY_MIGRATE_TO_GDRIVE_DATABASE) { + GlobalScope.launch { + val remoteDatabaseState = ApplicationBase.instance.remoteDatabaseState() + ApplicationBase.instance.notesDatabase().getAll().forEach { + remoteDatabaseState.notifyInsert(it) {} + } + ApplicationBase.instance.tagsDatabase().getAll().forEach { + remoteDatabaseState.notifyInsert(it) {} + } + ApplicationBase.instance.foldersDatabase().getAll().forEach { + remoteDatabaseState.notifyInsert(it) {} + } + } + } } private fun runTask(key: String, task: () -> Unit) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index 592c5ff1..d5bccd24 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase +import com.bijoysingh.quicknote.drive.GDriveRemoteDatabaseState import com.bijoysingh.quicknote.firebase.FirebaseRemoteDatabase import com.bijoysingh.quicknote.scarlet.ScarletConfig import com.github.bijoysingh.starter.prefs.Store @@ -24,6 +25,7 @@ class Scarlet : ApplicationBase() { companion object { var firebase: FirebaseRemoteDatabase? = null var gDrive: GDriveRemoteDatabase? = null + var gDriveDbState: GDriveRemoteDatabaseState? = null var gDriveConfig: Store? = null } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt index 4debe779..3c56e251 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.drive import android.content.Context import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount @@ -16,6 +17,7 @@ class GDriveAuthenticator { var account: GoogleSignInAccount? = null fun setup(context: Context) { + gDriveDbState = GDriveRemoteDatabaseState(context) GlobalScope.launch { account = GoogleSignIn.getLastSignedInAccount(context) hasAccountSetup.set(true) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index a7a207cf..c693e89e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity @@ -138,22 +139,7 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) gDrive?.init(mDriveServiceHelper!!) - GlobalScope.launch { - val database = gDrive?.gDriveDatabase - if (database === null) { - return@launch - } - ApplicationBase.instance.notesDatabase().getAll().forEach { - instance.noteActions(it).onlineSave(context) - } - ApplicationBase.instance.tagsDatabase().getAll().forEach { - instance.tagActions(it).onlineSave() - } - ApplicationBase.instance.foldersDatabase().getAll().forEach { - instance.folderActions(it).onlineSave() - } - finish() - } + finish() } override fun onConnectionFailed(p0: ConnectionResult) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index f0fa8ee1..c17ab46e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,21 +1,20 @@ package com.bijoysingh.quicknote.drive import android.content.Context +import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig -import com.bijoysingh.quicknote.database.* +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadData +import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.bijoysingh.quicknote.database.genGDriveUploadDatabase import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.auth.IPendingUploadListener -import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.NoteBuilder -import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils -import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.data.* import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.utils.log @@ -56,26 +55,30 @@ var sGDriveFirstSyncImage: Boolean get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) ?: false set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) ?: Unit -const val KEY_G_DRIVE_LAST_SYNC_DELTA_MS = 1000 * 60 -const val KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC = "g_drive_first_time_sync_last_sync" -var sGDriveLastSync: Long - get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, 0L) ?: 0L - set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_LAST_SYNC, value) ?: Unit - fun folderIdForFolderName(folderName: String, folderId: String = ""): String { val key = "g_drive_folder_if_for_$folderName" - return when (folderId.isEmpty()) { - true -> gDriveConfig?.get(key, "") ?: "" - false -> { - gDriveConfig?.put(key, folderId) - folderId - } + if (folderId.isEmpty()) { + // Get Condition + var storedValue = gDriveConfig?.get(key, "") ?: "" + if (storedValue == INVALID_FILE_ID) { + gDriveConfig?.put(key, "") + storedValue = "" + } + log("GDrive", "folderIdForFolderName($folderName, $storedValue") + return storedValue } + + if (folderId != INVALID_FILE_ID) { + gDriveConfig?.put(key, folderId) + } + return folderId } -class GDriveRemoteDatabase(val weakContext: WeakReference) { +class GDriveRemoteDatabase(private val weakContext: WeakReference) { + + lateinit var gDriveDbState: GDriveRemoteDatabaseState - var gDriveDatabase: GDriveUploadDataDao? = null + private var gDriveDatabase: GDriveUploadDataDao? = null private var isValidController: Boolean = true private var driveHelper: GDriveServiceHelper? = null @@ -88,6 +91,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { private var syncing = HashMap() private var syncListener: IPendingUploadListener? = null private var pendingSyncs: AtomicInteger = AtomicInteger(0) + private var databaseUpdateLambda: () -> Unit = { verifyAndNotifyPendingStateChange() } fun init(helper: GDriveServiceHelper) { val context = weakContext.get() @@ -98,6 +102,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { isValidController = true driveHelper = helper gDriveDatabase = genGDriveUploadDatabase(context) + gDriveDbState = Scarlet.gDriveDbState!! syncing[GDriveDataType.NOTE] = AtomicBoolean(false) syncing[GDriveDataType.TAG] = AtomicBoolean(false) @@ -183,7 +188,6 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { fun logout() { GlobalScope.launch { reset() - gDriveDatabase?.drop() gDriveConfig?.clearSync() } } @@ -325,79 +329,16 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { syncListener?.onPendingSyncsUpdate(true) } } + /** * Notify local changes to the notes */ - fun notifyInsert(data: Any) { + fun notifyChange() { if (!isValidController) { return } - when { - data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid) - data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) - data is Note -> notifyNoteInsertImpl(data) - else -> maybeThrow("notifyInsert called with unhandled data type") - } - } - - private fun notifyNoteInsertImpl(note: Note) { - val noteUuid = note.uuid - localDatabaseUpdate(GDriveDataType.NOTE, noteUuid) - localDatabaseUpdate(GDriveDataType.NOTE_META, noteUuid) - - val database = gDriveDatabase - if (database === null) { - return - } - - GlobalScope.launch { - val imageUUIDs = HashSet() - notifyImageIds(note) { imageUUIDs.add(it) } - - database.getByType(GDriveDataType.IMAGE.name) - .filter { - val uuid = toImageUUID(it.uuid) - uuid?.noteUuid == note.uuid && !imageUUIDs.contains(uuid) - }.forEach { - it.apply { - lastUpdateTimestamp = getTrueCurrentTime() - localStateDeleted = true - save(database) - } - } - - imageUUIDs.forEach { - val existing = database.getByUUID(GDriveDataType.IMAGE.name, it.name()) - if (existing !== null) { - return@launch - } - - GDriveUploadData().apply { - uuid = it.name() - type = GDriveDataType.IMAGE.name - lastUpdateTimestamp = getTrueCurrentTime() - localStateDeleted = false - save(database) - } - } - } - } - - fun notifyRemove(data: Any) { - if (!isValidController) { - return - } - - when { - data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, true) - data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, true) - data is Note -> { - localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, true) - localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, true) - } - else -> maybeThrow("notifyRemove called with unhandled data type") - } + verifyAndNotifyPendingStateChange() } /** @@ -426,10 +367,10 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { val pendingItems = gDriveDatabase?.getPendingByType(type.name) ?: emptyList() for (pendingItem in pendingItems) { - if (!notifyAttempt(type, pendingItem.uuid)) { + if (!gDriveDbState.notifyAttempt(type, pendingItem.uuid)) { // Think of a better solution here... // gDriveDatabase?.delete(pendingItem) - remoteDatabaseUpdate(type, pendingItem.uuid) + gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) continue } @@ -445,76 +386,13 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteInsert(type, pendingItem) !sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> remove(type, pendingItem) !sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteRemove(type, pendingItem) - !sameDelete && sameUpdateTime -> remoteDatabaseUpdate(type, pendingItem.uuid) // Ignoring + !sameDelete && sameUpdateTime -> gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) // Ignoring } } } syncing[type]?.set(false) } - /** - * Update the database about information - */ - - /** - * Notifies that an attempt to update this item was made. - * If this number is over 10, we will delete the item to prevent issues - * - * @return if false, the item will be deleted from the database - */ - private fun notifyAttempt(itemType: GDriveDataType, itemUUID: String): Boolean { - val database = gDriveDatabase - if (database === null) { - return false - } - - val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) - existing.apply { - attempts += 1 - save(database) - } - - return existing.attempts < 16 - } - - private fun localDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, removed: Boolean = false) { - GlobalScope.launch { - val database = gDriveDatabase - if (database === null) { - return@launch - } - - log("GDrive", "localDatabaseUpdate(${itemType.name}, $itemUUID)") - val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) - existing.apply { - attempts = 0 - lastUpdateTimestamp = Math.max(gDriveUpdateTimestamp + 1, getTrueCurrentTime()) - localStateDeleted = removed - save(database) - } - verifyAndNotifyPendingStateChange() - } - } - - private fun remoteDatabaseUpdate(itemType: GDriveDataType, itemUUID: String) { - GlobalScope.launch { - val database = gDriveDatabase - if (database === null) { - return@launch - } - - log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") - val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) - existing.apply { - attempts = 0 - lastUpdateTimestamp = gDriveUpdateTimestamp - localStateDeleted = gDriveStateDeleted - save(database) - } - verifyAndNotifyPendingStateChange() - } - } - /** * Core Data Functions */ @@ -602,7 +480,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { temporaryNote.description = itemDescription IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } @@ -620,7 +498,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { temporaryNote.mergeMetas(item) IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } @@ -631,7 +509,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { try { val item = Gson().fromJson(it, ExportableTag::class.java) IRemoteDatabaseUtils.onRemoteInsert(context, item) - remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } @@ -642,7 +520,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { try { val item = Gson().fromJson(it, ExportableFolder::class.java) IRemoteDatabaseUtils.onRemoteInsert(context, item) - remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } @@ -653,14 +531,14 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { if (imageUUID !== null) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) if (imageFile.exists()) { - remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) decrementPendingSyncs() return } driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { if (it.result == true) { - remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) } decrementPendingSyncs() } @@ -684,7 +562,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { when (type) { GDriveDataType.NOTE -> { IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) - remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid) + gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, databaseUpdateLambda) } GDriveDataType.NOTE_META -> { } // Should never happen as note is handling this deletion @@ -698,7 +576,7 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { } } } - remoteDatabaseUpdate(type, data.uuid) + gDriveDbState.remoteDatabaseUpdate(type, data.uuid, databaseUpdateLambda) decrementPendingSyncs() } @@ -715,14 +593,4 @@ class GDriveRemoteDatabase(val weakContext: WeakReference) { decrementPendingSyncs() } } - - private fun notifyImageIds(note: Note, onImageUUID: (ImageUUID) -> Unit) { - val imageIds = note.getFormats() - .filter { it.formatType == FormatType.IMAGE } - .map { it.text } - .toSet() - imageIds.forEach { - onImageUUID(ImageUUID(note.uuid, it)) - } - } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt new file mode 100644 index 00000000..9eb58efa --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt @@ -0,0 +1,162 @@ +package com.bijoysingh.quicknote.drive + +import android.content.Context +import com.bijoysingh.quicknote.database.* +import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.database.room.tag.Tag +import com.maubis.scarlet.base.support.utils.log +import com.maubis.scarlet.base.support.utils.maybeThrow +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { + + init { + gDriveDatabase = genGDriveUploadDatabase(context) + } + + /** + * Notify local changes to the notes + */ + override fun notifyInsert(data: Any, onExecution: () -> Unit) { + when { + data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, onExecution) + data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, onExecution) + data is Note -> notifyNoteInsertImpl(data, onExecution) + else -> maybeThrow("notifyInsert called with unhandled data type") + } + } + + private fun notifyNoteInsertImpl(note: Note, onExecution: () -> Unit) { + val noteUuid = note.uuid + localDatabaseUpdate(GDriveDataType.NOTE, noteUuid, onExecution) + localDatabaseUpdate(GDriveDataType.NOTE_META, noteUuid, onExecution) + + val database = gDriveDatabase + if (database === null) { + return + } + + GlobalScope.launch { + val imageUUIDs = HashSet() + notifyImageIds(note) { imageUUIDs.add(it) } + + database.getByType(GDriveDataType.IMAGE.name) + .filter { + val uuid = toImageUUID(it.uuid) + uuid?.noteUuid == note.uuid && !imageUUIDs.contains(uuid) + }.forEach { + it.apply { + lastUpdateTimestamp = getTrueCurrentTime() + localStateDeleted = true + save(database) + } + } + + imageUUIDs.forEach { + val existing = database.getByUUID(GDriveDataType.IMAGE.name, it.name()) + if (existing !== null) { + return@launch + } + + GDriveUploadData().apply { + uuid = it.name() + type = GDriveDataType.IMAGE.name + lastUpdateTimestamp = getTrueCurrentTime() + localStateDeleted = false + save(database) + } + } + } + } + + override fun notifyRemove(data: Any, onExecution: () -> Unit) { + when { + data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, onExecution, true) + data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, onExecution, true) + data is Note -> { + localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, onExecution, true) + localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, onExecution, true) + } + else -> maybeThrow("notifyRemove called with unhandled data type") + } + } + + + /** + * Notifies that an attempt to update this item was made. + * If this number is over 10, we will delete the item to prevent issues + * + * @return if false, the item will be deleted from the database + */ + fun notifyAttempt(itemType: GDriveDataType, itemUUID: String): Boolean { + val database = gDriveDatabase + if (database === null) { + return false + } + + val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + existing.apply { + attempts += 1 + save(database) + } + + return existing.attempts < 16 + } + + fun remoteDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, onExecution: () -> Unit) { + GlobalScope.launch { + val database = gDriveDatabase + if (database === null) { + return@launch + } + + log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") + val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + existing.apply { + attempts = 0 + lastUpdateTimestamp = gDriveUpdateTimestamp + localStateDeleted = gDriveStateDeleted + save(database) + } + onExecution() + } + } + + fun localDatabaseUpdate( + itemType: GDriveDataType, + itemUUID: String, + onExecution: () -> Unit, + removed: Boolean = false) { + GlobalScope.launch { + val database = gDriveDatabase + if (database === null) { + return@launch + } + + log("GDrive", "localDatabaseUpdate(${itemType.name}, $itemUUID)") + val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + existing.apply { + attempts = 0 + lastUpdateTimestamp = Math.max(gDriveUpdateTimestamp + 1, getTrueCurrentTime()) + localStateDeleted = removed + save(database) + } + onExecution() + } + } + + private fun notifyImageIds(note: Note, onImageUUID: (ImageUUID) -> Unit) { + val imageIds = note.getFormats() + .filter { it.formatType == FormatType.IMAGE } + .map { it.text } + .toSet() + imageIds.forEach { + onImageUUID(ImageUUID(note.uuid, it)) + } + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 66fbdcf4..c3cdbbb3 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -16,7 +16,7 @@ abstract class GDriveRemoteFolderBase( val onPendingSyncComplete: () -> Unit) { protected fun notifyDriveData(file: File, deleted: Boolean = false) { - val modifiedTime = file.modifiedTime?.value ?: 0L + val modifiedTime = file.modifiedTime?.value ?: file.modifiedByMeTime?.value ?: 0L notifyDriveData(file.id, file.name, modifiedTime, deleted) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 821721d8..26696606 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -113,15 +113,28 @@ class GDriveRemoteImageFolder( } if (!contentFiles.containsKey(id)) { - onPendingSyncComplete() + GlobalScope.launch { + val existing = database.getByUUID(dataType.name, id.name()) + if (existing !== null) { + database.delete(existing) + } + onPendingSyncComplete() + } return } - helper.removeFileOrFolder(contentFiles[id] ?: INVALID_FILE_ID) - .addOnCompleteListener { - contentFiles.remove(id) - onPendingSyncComplete() - } - .addOnCanceledListener { onPendingSyncComplete() } + GlobalScope.launch { + val fuid = contentFiles[id] ?: INVALID_FILE_ID + val existing = database.getByUUID(dataType.name, id.name()) + val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() + + helper.removeFileOrFolder(fuid) + .addOnCompleteListener { + notifyDriveData(fuid, id.name(), timestamp, true) + contentFiles.remove(id) + onPendingSyncComplete() + } + .addOnCanceledListener { onPendingSyncComplete() } + } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index ea5315eb..aac0da2a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -98,6 +98,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) .setModifiedTime(DateTime(updateTime)) + .setModifiedByMeTime(DateTime(updateTime)) .setName(name) val contentStream = ByteArrayContent.fromString("text/plain", contentToSave) mDriveService.files().create(metadata, contentStream).execute() @@ -111,6 +112,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) .setModifiedTime(DateTime(updateTime)) + .setModifiedByMeTime(DateTime(updateTime)) .setName(name) val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) mDriveService.files().create(metadata, mediaContent).execute() @@ -120,9 +122,11 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun createFolder(parentUid: String, folderName: String): Task { log("GDrive", "createFolder($parentUid, $folderName)") return execute("createFolder", Callable { + val timestamp = DateTime(getTrueCurrentTime()) val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) - .setModifiedTime(DateTime(getTrueCurrentTime())) + .setModifiedTime(timestamp) + .setModifiedByMeTime(timestamp) .setName(folderName) if (!parentUid.isEmpty()) { metadata.parents = listOf(parentUid) @@ -160,7 +164,10 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { log("GDrive", "saveFile($fileId, $name)") return execute("saveFile", Callable { - val metadata = File().setModifiedTime(DateTime(updateTime)).setName(name) + val metadata = File() + .setModifiedTime(DateTime(updateTime)) + .setModifiedByMeTime(DateTime(updateTime)) + .setName(name) val contentStream = ByteArrayContent.fromString("text/plain", content) mDriveService.files().update(fileId, metadata, contentStream).execute() }) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index cea4f87e..badf010d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -4,6 +4,7 @@ import android.content.Context import android.support.v7.app.AppCompatActivity import com.bijoysingh.quicknote.BuildConfig import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.openIfNeeded import com.bijoysingh.quicknote.firebase.support.RemoteConfigFetcher import com.maubis.scarlet.base.config.MaterialNoteConfig @@ -12,6 +13,7 @@ import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.core.folder.IFolderActor import com.maubis.scarlet.base.core.note.INoteActor import com.maubis.scarlet.base.core.tag.ITagActor +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag @@ -29,6 +31,8 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { override fun remoteConfigFetcher(): IRemoteConfigFetcher = RemoteConfigFetcher() + override fun remoteDatabaseState(): IRemoteDatabaseState = gDriveDbState!! + override fun appFlavor(): Flavor = when (BuildConfig.FLAVOR) { "lite" -> Flavor.LITE "full" -> Flavor.PRO diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index 734a1055..a9fe0737 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -1,18 +1,23 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.maubis.scarlet.base.core.folder.MaterialFolderActor import com.maubis.scarlet.base.database.room.folder.Folder class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { + private val notifyChange: () -> Unit = { + gDrive?.notifyChange() + } + override fun onlineSave() { super.onlineSave() - gDrive?.notifyInsert(folder) + gDriveDbState?.notifyInsert(folder, notifyChange) } override fun delete() { super.delete() - gDrive?.notifyRemove(folder) + gDriveDbState?.notifyRemove(folder, notifyChange) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index b59a9186..5b683166 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -1,21 +1,27 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context +import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.maubis.scarlet.base.core.note.MaterialNoteActor import com.maubis.scarlet.base.database.room.note.Note class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { + private val notifyChange: () -> Unit = { + gDrive?.notifyChange() + } + override fun onlineSave(context: Context) { super.onlineSave(context) - gDrive?.notifyInsert(note) + gDriveDbState?.notifyInsert(note, notifyChange) } override fun onlineDelete(context: Context) { super.onlineDelete(context) - gDrive?.notifyRemove(note) + gDriveDbState?.notifyRemove(note, notifyChange) } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 874a704c..4e818e8b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -1,18 +1,24 @@ package com.bijoysingh.quicknote.scarlet +import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.maubis.scarlet.base.core.tag.MaterialTagActor import com.maubis.scarlet.base.database.room.tag.Tag class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { + private val notifyChange: () -> Unit = { + gDrive?.notifyChange() + } + override fun onlineSave() { super.onlineSave() - gDrive?.notifyInsert(tag) + gDriveDbState?.notifyInsert(tag, notifyChange) } override fun delete() { super.delete() - gDrive?.notifyRemove(tag) + gDriveDbState?.notifyRemove(tag, notifyChange) } } \ No newline at end of file From 7b6d207b5de741e8d57f922d58780572ee15e7d1 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 15 Jun 2019 22:05:23 +0100 Subject: [PATCH 039/134] [GDrive] Resolving Sync Issues in no network conditions --- .../com/maubis/scarlet/base/MainActivity.kt | 16 +-- .../base/config/auth/IAuthenticator.kt | 2 +- .../base/config/auth/NullAuthenticator.kt | 2 +- .../base/support/database/MigrationUtils.kt | 2 +- .../3.json | 12 ++- .../4.json | 97 +++++++++++++++++++ .../quicknote/database/GDriveUploadData.kt | 5 + .../database/GDriveUploadDatabase.kt | 2 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 88 +++++++++-------- .../drive/GDriveRemoteDatabaseState.kt | 16 ++- .../quicknote/drive/GDriveRemoteFolder.kt | 60 +++++++++--- .../quicknote/drive/GDriveRemoteFolderBase.kt | 2 +- .../drive/GDriveRemoteImageFolder.kt | 42 ++++++-- .../quicknote/drive/GDriveServiceHelper.kt | 4 - .../quicknote/scarlet/ScarletAuthenticator.kt | 15 ++- 15 files changed, 280 insertions(+), 85 deletions(-) create mode 100644 scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/4.json diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index b46d5116..5e3027f2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -56,13 +56,11 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.unifiedFolderSearchSynchronous import com.maubis.scarlet.base.support.unifiedSearchSynchronous -import com.maubis.scarlet.base.support.utils.maybeThrow import com.maubis.scarlet.base.support.utils.shouldShowWhatsNewSheet import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.search_toolbar_main.* import kotlinx.android.synthetic.main.toolbar_trash_info.* import kotlinx.coroutines.* -import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean class MainActivity : ThemedActivity(), INoteOptionSheetActivity { @@ -75,7 +73,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { private lateinit var receiver: BroadcastReceiver private lateinit var tagAndColorPicker: TagsAndColorPickerViewHolder - private var lastSyncState: AtomicBoolean = AtomicBoolean(false) + private var lastSyncPending: AtomicBoolean = AtomicBoolean(false) private var lastSyncHappening: AtomicBoolean = AtomicBoolean(false) var config: SearchConfig = SearchConfig(mode = HomeNavigationState.DEFAULT) @@ -337,7 +335,7 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { return } - if (lastSyncState.getAndSet(isSyncPending) == isSyncPending + if (lastSyncPending.getAndSet(isSyncPending) == isSyncPending && lastSyncHappening.getAndSet(isSyncHappening) == isSyncHappening) { return } @@ -355,11 +353,13 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { MainActivitySyncingNow.create(componentContext) .isSyncHappening(isSyncHappening) .onClick { - instance.authenticator().requestSync() + if (!lastSyncHappening.get()) { + instance.authenticator().requestSync(true) + } } .build())) if (!isSyncHappening && isSyncPending) { - instance.authenticator().requestSync() + instance.authenticator().requestSync(false) } } } @@ -387,14 +387,14 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { notifyDisabledSync() instance.authenticator().setPendingUploadListener(object : IPendingUploadListener { override fun onPendingSyncsUpdate(isSyncHappening: Boolean) { - notifySyncingInformation(isSyncHappening, lastSyncState.get()) + notifySyncingInformation(isSyncHappening, lastSyncPending.get()) } override fun onPendingStateUpdate(isDataSyncPending: Boolean) { notifySyncingInformation(lastSyncHappening.get(), isDataSyncPending) } }) - instance.authenticator().requestSync() + instance.authenticator().requestSync(false) } fun resetAndSetupData() { diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index a38a059a..5a74f87a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -22,7 +22,7 @@ interface IAuthenticator { fun setPendingUploadListener(listener: IPendingUploadListener?) - fun requestSync() + fun requestSync(forced: Boolean) fun logout() } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index 48dffc15..6eceb255 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -23,5 +23,5 @@ class NullAuthenticator : IAuthenticator { override fun setPendingUploadListener(listener: IPendingUploadListener?) {} - override fun requestSync() {} + override fun requestSync(forced: Boolean) {} } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 6dd4ea53..11ec9add 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -24,7 +24,7 @@ const val KEY_MIGRATE_THEME = "KEY_MIGRATE_THEME" const val KEY_MIGRATE_DEFAULT_VALUES = "KEY_MIGRATE_DEFAULT_VALUES" const val KEY_MIGRATE_REMINDERS = "KEY_MIGRATE_REMINDERS" const val KEY_MIGRATE_IMAGES = "KEY_MIGRATE_IMAGES" -const val KEY_MIGRATE_TO_GDRIVE_DATABASE = "KEY_MIGRATE_TO_GDRIVE_DATABASE" +const val KEY_MIGRATE_TO_GDRIVE_DATABASE = "KEY_MIGRATE_TO_GDRIVE_DATABASE_v2" class Migrator(val context: Context) { diff --git a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json index 3a835e9a..ef6962c9 100644 --- a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json +++ b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/3.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "b21b94b1e0f1525de8c4bbe5191c125c", + "identityHash": "a64a14da15936f694f2119342656f35e", "entities": [ { "tableName": "gdrive_upload", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL, `attempts` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL, `attempts` INTEGER NOT NULL, `lastAttemptTime` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", @@ -61,6 +61,12 @@ "columnName": "attempts", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "lastAttemptTime", + "columnName": "lastAttemptTime", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -85,7 +91,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b21b94b1e0f1525de8c4bbe5191c125c\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a64a14da15936f694f2119342656f35e\")" ] } } \ No newline at end of file diff --git a/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/4.json b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/4.json new file mode 100644 index 00000000..7ea729d4 --- /dev/null +++ b/scarlet/schemas/com.bijoysingh.quicknote.database.GDriveUploadDatabase/4.json @@ -0,0 +1,97 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "a64a14da15936f694f2119342656f35e", + "entities": [ + { + "tableName": "gdrive_upload", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL, `attempts` INTEGER NOT NULL, `lastAttemptTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdateTimestamp", + "columnName": "lastUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localStateDeleted", + "columnName": "localStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gDriveUpdateTimestamp", + "columnName": "gDriveUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gDriveStateDeleted", + "columnName": "gDriveStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attempts", + "columnName": "attempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastAttemptTime", + "columnName": "lastAttemptTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_gdrive_upload_uuid_type", + "unique": true, + "columnNames": [ + "uuid", + "type" + ], + "createSql": "CREATE UNIQUE INDEX `index_gdrive_upload_uuid_type` ON `${TABLE_NAME}` (`uuid`, `type`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a64a14da15936f694f2119342656f35e\")" + ] + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index d345ee36..91e3922d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -34,6 +34,8 @@ class GDriveUploadData { var attempts: Long = 0L + var lastAttemptTime: Long = 0L + @Ignore fun save(dao: GDriveUploadDataDao) { if (uuid.isBlank() || type.isBlank()) { @@ -72,6 +74,9 @@ interface GDriveUploadDataDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(note: GDriveUploadData): Long + @Query("UPDATE gdrive_upload SET attempts = 0") + fun resetAttempts() + @Delete fun delete(note: GDriveUploadData) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt index 32ceacd0..fe561d4f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt @@ -15,7 +15,7 @@ fun genGDriveUploadDatabase(context: Context): GDriveUploadDataDao? { return gDriveDatabase } -@Database(entities = [GDriveUploadData::class], version = 3) +@Database(entities = [GDriveUploadData::class], version = 4) abstract class GDriveUploadDatabase : RoomDatabase() { abstract fun drive(): GDriveUploadDataDao } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index c17ab46e..e903d455 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -115,7 +115,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { decrementPendingSyncs() }, + onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { it }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() @@ -125,7 +125,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { decrementPendingSyncs() }, + onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() @@ -135,7 +135,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { decrementPendingSyncs() }, + onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() @@ -145,7 +145,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { decrementPendingSyncs() }, + onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() @@ -155,7 +155,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { decrementPendingSyncs() }) + onPendingSyncComplete = { action -> decrementPendingSyncs(action) }) GlobalScope.launch { val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) @@ -196,14 +196,16 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { * Initialisation Methods */ private fun initSubRootFolder(folderName: String, folderId: String) { - incrementPendingSyncs() + val logInfo = "initSubRootFolder($folderName, $folderId)" + log("GDriveRemote", logInfo) + incrementPendingSyncs(logInfo) when (folderName) { FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNote) { GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) } sGDriveFirstSyncNote = true } - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } FOLDER_NAME_NOTES_META -> { notesMetaSync?.initContentFolderId(folderId) { @@ -211,7 +213,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } sGDriveFirstSyncNoteMeta = true } - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } notesMetaSync?.initDeletedFolderId(INVALID_FILE_ID) {} } @@ -220,30 +222,30 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { GlobalScope.launch { resyncDataSync(GDriveDataType.TAG) } sGDriveFirstSyncTag = true } - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncFolder) { GlobalScope.launch { resyncDataSync(GDriveDataType.FOLDER) } sGDriveFirstSyncFolder = true } - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncImage) { GlobalScope.launch { resyncDataSync(GDriveDataType.IMAGE) } sGDriveFirstSyncImage = true } - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) { - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) { - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) { - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } } } @@ -314,18 +316,18 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } @Synchronized - private fun decrementPendingSyncs() { - pendingSyncs.decrementAndGet() - if (pendingSyncs.get() <= 0) { + private fun decrementPendingSyncs(action: String) { + log("GDriveRemote", "pendingSync: decrement to ${pendingSyncs.get() - 1}, action: $action") + if (pendingSyncs.decrementAndGet() <= 0) { pendingSyncs.set(0) syncListener?.onPendingSyncsUpdate(false) } } @Synchronized - private fun incrementPendingSyncs() { - pendingSyncs.incrementAndGet() - if (pendingSyncs.get() >= 1) { + private fun incrementPendingSyncs(action: String) { + log("GDriveRemote", "pendingSync: increment to ${pendingSyncs.get() + 1}, action: $action") + if (pendingSyncs.incrementAndGet() >= 1) { syncListener?.onPendingSyncsUpdate(true) } } @@ -368,9 +370,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val pendingItems = gDriveDatabase?.getPendingByType(type.name) ?: emptyList() for (pendingItem in pendingItems) { if (!gDriveDbState.notifyAttempt(type, pendingItem.uuid)) { - // Think of a better solution here... - // gDriveDatabase?.delete(pendingItem) - gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) continue } @@ -402,33 +401,33 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { return } - log("GDriveRemote", "insert(${type.name}, ${data.uuid})") - incrementPendingSyncs() + val logInfo = "insert(${type.name}, ${data.uuid})" + incrementPendingSyncs(logInfo) when (type) { GDriveDataType.NOTE -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { notesSync?.insert(data.uuid, this) - } ?: decrementPendingSyncs() + } ?: decrementPendingSyncs(logInfo) } GDriveDataType.NOTE_META -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { notesMetaSync?.insert(data.uuid, this) - } ?: decrementPendingSyncs() + } ?: decrementPendingSyncs(logInfo) } GDriveDataType.TAG -> { ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { tagsSync?.insert(this.uuid, this) - } ?: decrementPendingSyncs() + } ?: decrementPendingSyncs(logInfo) } GDriveDataType.FOLDER -> { ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { foldersSync?.insert(this.uuid, this) - } ?: decrementPendingSyncs() + } ?: decrementPendingSyncs(logInfo) } GDriveDataType.IMAGE -> { toImageUUID(data.uuid)?.apply { imageSync?.insert(this) - } ?: decrementPendingSyncs() + } ?: decrementPendingSyncs(logInfo) } } } @@ -438,8 +437,9 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { return } - log("GDriveRemote", "remove(${type.name}, ${data.uuid})") - incrementPendingSyncs() + val logInfo = "remove(${type.name}, ${data.uuid})" + log("GDriveRemote", logInfo) + incrementPendingSyncs(logInfo) val uuid = data.uuid when (type) { GDriveDataType.NOTE -> notesSync?.delete(uuid) @@ -450,7 +450,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val imageUUID = toImageUUID(uuid) when { imageUUID !== null -> imageSync?.delete(imageUUID) - else -> decrementPendingSyncs() + else -> decrementPendingSyncs(logInfo) } } } @@ -466,8 +466,9 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { return } - log("GDriveRemote", "onRemoteInsert(${type.name}, ${data.uuid})") - incrementPendingSyncs() + val logInfo = "onRemoteInsert(${type.name}, ${data.uuid})" + log("GDriveRemote", logInfo) + incrementPendingSyncs(logInfo) when (type) { GDriveDataType.NOTE -> { onRemoteInsertImpl(data.fileId) { @@ -483,6 +484,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) + decrementPendingSyncs(logInfo) } } } @@ -501,6 +503,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) + decrementPendingSyncs(logInfo) } } } @@ -512,6 +515,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) + decrementPendingSyncs(logInfo) } } } @@ -523,6 +527,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) + decrementPendingSyncs(logInfo) } } } @@ -532,7 +537,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) if (imageFile.exists()) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) - decrementPendingSyncs() + decrementPendingSyncs(logInfo) return } @@ -540,7 +545,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { if (it.result == true) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) } - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } } } @@ -557,8 +562,9 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { return } - log("GDriveRemote", "onRemoteRemove(${type.name}, ${data.uuid})") - incrementPendingSyncs() + val logInfo ="onRemoteRemove(${type.name}, ${data.uuid})" + log("GDriveRemote", logInfo) + incrementPendingSyncs(logInfo) when (type) { GDriveDataType.NOTE -> { IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) @@ -577,7 +583,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } gDriveDbState.remoteDatabaseUpdate(type, data.uuid, databaseUpdateLambda) - decrementPendingSyncs() + decrementPendingSyncs(logInfo) } /** @@ -590,7 +596,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { if (data !== null) { onDataAvailable(data) } - decrementPendingSyncs() + decrementPendingSyncs("onRemoteInsertImpl") } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt index 9eb58efa..c86e97a0 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt @@ -100,12 +100,20 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { } val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + val lastAttemptedTime = existing.lastAttemptTime + + // If it fails 8 times, only re-attempt after hour. This handles situations like no-network conditions + val reAttempt = (existing.attempts >= 8 && (getTrueCurrentTime() - lastAttemptedTime > 1000 * 60 * 60)) existing.apply { - attempts += 1 + attempts = when { + (attempts < 8) -> attempts + 1 + reAttempt -> 0 + else -> attempts + } + lastAttemptTime = getTrueCurrentTime() save(database) } - - return existing.attempts < 16 + return existing.attempts < 8 } fun remoteDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, onExecution: () -> Unit) { @@ -119,6 +127,7 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { attempts = 0 + lastAttemptTime = 0 lastUpdateTimestamp = gDriveUpdateTimestamp localStateDeleted = gDriveStateDeleted save(database) @@ -142,6 +151,7 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { attempts = 0 + lastAttemptTime = 0 lastUpdateTimestamp = Math.max(gDriveUpdateTimestamp + 1, getTrueCurrentTime()) localStateDeleted = removed save(database) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index f036e35a..45cf21e0 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -2,7 +2,6 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType import com.bijoysingh.quicknote.database.GDriveUploadDataDao -import com.maubis.scarlet.base.support.utils.log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -13,10 +12,12 @@ class GDriveRemoteFolder( database: GDriveUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit, - onPendingSyncComplete: () -> Unit, + onPendingSyncComplete: (String) -> Unit, val serialiser: (T) -> String, val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { + var networkOrAbsoluteFailure = AtomicBoolean(false) + var contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID var contentPendingActions = emptySet().toMutableSet() @@ -30,10 +31,16 @@ class GDriveRemoteFolder( val duplicateFilesToDelete: MutableList = emptyList().toMutableList() fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { + val logInfo = "initContentFolderId($fUid)" GlobalScope.launch(Dispatchers.IO) { contentLoading.set(true) contentFolderUid = fUid helper.getFilesInFolder(contentFolderUid).addOnCompleteListener { + if (it.result === null) { + // Something bad happened, probably network failure etc + networkOrAbsoluteFailure.set(true) + } + val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() files.forEach { file -> @@ -51,11 +58,16 @@ class GDriveRemoteFolder( GlobalScope.launch { executeAllDuplicateDeletion() } GlobalScope.launch { executeInsertPendingActions() } GlobalScope.launch { onLoaded() } + }.addOnFailureListener { + onPendingSyncComplete(logInfo) + }.addOnCanceledListener { + onPendingSyncComplete(logInfo) } } } fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { + val logInfo = "initDeletedFolderId($fUid)" if (fUid == INVALID_FILE_ID) { deletedLoading.set(false) GlobalScope.launch { executeDeletePendingActions() } @@ -67,6 +79,11 @@ class GDriveRemoteFolder( deletedLoading.set(true) deletedFolderUid = fUid helper.getFilesInFolder(deletedFolderUid).addOnCompleteListener { + if (it.result === null) { + // Something bad happened, probably network failure etc + networkOrAbsoluteFailure.set(true) + } + val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() files.forEach { file -> @@ -84,6 +101,10 @@ class GDriveRemoteFolder( GlobalScope.launch { executeAllDuplicateDeletion() } GlobalScope.launch { executeDeletePendingActions() } GlobalScope.launch { onLoaded() } + }.addOnFailureListener { + onPendingSyncComplete(logInfo) + }.addOnCanceledListener { + onPendingSyncComplete(logInfo) } } } @@ -118,17 +139,22 @@ class GDriveRemoteFolder( * Insert the file on the server based on the insertion on the local device */ fun insert(uuid: String, item: T) { + val logInfo = "insert($uuid)" if (contentLoading.get()) { contentPendingActions.add(uuid) return } + if (networkOrAbsoluteFailure.get()) { + onPendingSyncComplete(logInfo) + return + } + val data = serialiser(item) val fileId = contentFiles[uuid] val existing = database.getByUUID(dataType.name, uuid) val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() - log("GDriveFolder", "uuid=$uuid efid=${existing?.fileId} ets=${existing?.lastUpdateTimestamp} :: fid=$fileId, ts=$timestamp") if (fileId !== null) { helper.saveFile(fileId, uuid, data, timestamp) .addOnCompleteListener { @@ -136,9 +162,10 @@ class GDriveRemoteFolder( if (file !== null) { notifyDriveData(file.id, uuid, timestamp) } - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete() } + .addOnFailureListener { onPendingSyncComplete(logInfo) } + .addOnCanceledListener { onPendingSyncComplete(logInfo) } return } helper.createFileWithData(contentFolderUid, uuid, data, timestamp) @@ -148,9 +175,10 @@ class GDriveRemoteFolder( contentFiles[uuid] = file.id notifyDriveData(file.id, uuid, timestamp) } - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete() } + .addOnFailureListener { onPendingSyncComplete(logInfo) } + .addOnCanceledListener { onPendingSyncComplete(logInfo) } } @@ -158,11 +186,17 @@ class GDriveRemoteFolder( * Delete the file on the server based on removal on the local device */ fun delete(uuid: String) { + val logInfo = "delete($uuid)" if (deletedLoading.get() || contentLoading.get()) { deletedPendingActions.add(uuid) return } + if (networkOrAbsoluteFailure.get()) { + onPendingSyncComplete(logInfo) + return + } + val existingFileUid = contentFiles[uuid] if (existingFileUid === null) { GlobalScope.launch { @@ -171,7 +205,7 @@ class GDriveRemoteFolder( database.delete(existing) onPendingChange() } - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } return } @@ -180,7 +214,7 @@ class GDriveRemoteFolder( .addOnCompleteListener { contentFiles.remove(uuid) if (deletedFolderUid == INVALID_FILE_ID) { - onPendingSyncComplete() + onPendingSyncComplete(logInfo) return@addOnCompleteListener } @@ -194,11 +228,13 @@ class GDriveRemoteFolder( deletedFiles[uuid] = file.id notifyDriveData(file.id, uuid, timestamp, true) } - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete() } + .addOnFailureListener { onPendingSyncComplete(logInfo) } + .addOnCanceledListener { onPendingSyncComplete(logInfo) } } } - .addOnCanceledListener { onPendingSyncComplete() } + .addOnFailureListener { onPendingSyncComplete(logInfo) } + .addOnCanceledListener { onPendingSyncComplete(logInfo) } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index c3cdbbb3..970f4848 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -13,7 +13,7 @@ abstract class GDriveRemoteFolderBase( val database: GDriveUploadDataDao, val helper: GDriveServiceHelper, val onPendingChange: () -> Unit, - val onPendingSyncComplete: () -> Unit) { + val onPendingSyncComplete: (String) -> Unit) { protected fun notifyDriveData(file: File, deleted: Boolean = false) { val modifiedTime = file.modifiedTime?.value ?: file.modifiedByMeTime?.value ?: 0L diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 26696606..4c61ed19 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -42,7 +42,9 @@ class GDriveRemoteImageFolder( database: GDriveUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit, - onPendingSyncComplete: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { + onPendingSyncComplete: (String) -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { + + var networkOrAbsoluteFailure = AtomicBoolean(false) val contentLoading = AtomicBoolean(true) var contentFolderUid: String = INVALID_FILE_ID @@ -52,9 +54,15 @@ class GDriveRemoteImageFolder( val deletedPendingActions = emptySet().toMutableSet() fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { + val logInfo = "initContentFolderId($fUid)" contentFolderUid = fUid GlobalScope.launch(Dispatchers.IO) { helper.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { + if (it.result === null) { + // Something bad happened, probably network failure etc + networkOrAbsoluteFailure.set(true) + } + val imageFiles = it.result?.files if (imageFiles !== null) { imageFiles.forEach { imageFile -> @@ -65,18 +73,28 @@ class GDriveRemoteImageFolder( } } contentLoading.set(false) - GlobalScope.launch { onLoaded() } } + GlobalScope.launch { onLoaded() } + }.addOnFailureListener { + onPendingSyncComplete(logInfo) + }.addOnCanceledListener { + onPendingSyncComplete(logInfo) } } } fun insert(id: ImageUUID) { + val logInfo = "insert($id)" if (contentLoading.get()) { contentPendingActions.add(id) return } + if (networkOrAbsoluteFailure.get()) { + onPendingSyncComplete(logInfo) + return + } + if (contentFiles.containsKey(id)) { GlobalScope.launch { database.getByUUID(dataType.name, id.name())?.apply { @@ -85,7 +103,7 @@ class GDriveRemoteImageFolder( save(database) } onPendingChange() - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } return } @@ -101,24 +119,31 @@ class GDriveRemoteImageFolder( contentFiles[id] = file.id notifyDriveData(file.id, gDriveUUID, timestamp) } - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete() } + .addOnFailureListener { onPendingSyncComplete(logInfo) } + .addOnCanceledListener { onPendingSyncComplete(logInfo) } } fun delete(id: ImageUUID) { + val logInfo = "delete($id)" if (contentLoading.get()) { deletedPendingActions.add(id) return } + if (networkOrAbsoluteFailure.get()) { + onPendingSyncComplete(logInfo) + return + } + if (!contentFiles.containsKey(id)) { GlobalScope.launch { val existing = database.getByUUID(dataType.name, id.name()) if (existing !== null) { database.delete(existing) } - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } return } @@ -132,9 +157,10 @@ class GDriveRemoteImageFolder( .addOnCompleteListener { notifyDriveData(fuid, id.name(), timestamp, true) contentFiles.remove(id) - onPendingSyncComplete() + onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete() } + .addOnFailureListener { onPendingSyncComplete(logInfo) } + .addOnCanceledListener { onPendingSyncComplete(logInfo) } } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index aac0da2a..5855bfe1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -98,7 +98,6 @@ class GDriveServiceHelper(private val mDriveService: Drive) { .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) .setModifiedTime(DateTime(updateTime)) - .setModifiedByMeTime(DateTime(updateTime)) .setName(name) val contentStream = ByteArrayContent.fromString("text/plain", contentToSave) mDriveService.files().create(metadata, contentStream).execute() @@ -112,7 +111,6 @@ class GDriveServiceHelper(private val mDriveService: Drive) { .setParents(listOf(folderId)) .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) .setModifiedTime(DateTime(updateTime)) - .setModifiedByMeTime(DateTime(updateTime)) .setName(name) val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) mDriveService.files().create(metadata, mediaContent).execute() @@ -126,7 +124,6 @@ class GDriveServiceHelper(private val mDriveService: Drive) { val metadata = File() .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) .setModifiedTime(timestamp) - .setModifiedByMeTime(timestamp) .setName(folderName) if (!parentUid.isEmpty()) { metadata.parents = listOf(parentUid) @@ -166,7 +163,6 @@ class GDriveServiceHelper(private val mDriveService: Drive) { return execute("saveFile", Callable { val metadata = File() .setModifiedTime(DateTime(updateTime)) - .setModifiedByMeTime(DateTime(updateTime)) .setName(name) val contentStream = ByteArrayContent.fromString("text/plain", content) mDriveService.files().update(fileId, metadata, contentStream).execute() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 7ad44d65..69b3993a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.database.gDriveDatabase import com.bijoysingh.quicknote.drive.GDriveAuthenticator import com.bijoysingh.quicknote.drive.GDriveLoginActivity import com.bijoysingh.quicknote.drive.GDriveLogoutActivity @@ -12,6 +13,8 @@ import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.auth.IPendingUploadListener +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" var sGDriveLoggedIn: Boolean @@ -67,7 +70,17 @@ class ScarletAuthenticator() : IAuthenticator { } } - override fun requestSync() { + override fun requestSync(forced: Boolean) { + if (forced) { + GlobalScope.launch { + gDriveDatabase?.resetAttempts() + if (sGDriveLoggedIn) { + gDrive?.resync() + } + } + return + } + if (sGDriveLoggedIn) { gDrive?.resync() } From 33cc2dc3ff8a6a01cf40fd21c1a18a736bc7e9e0 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 19 Jun 2019 00:49:03 +0100 Subject: [PATCH 040/134] [GDrive] Alowing forced refreshes --- app/build.gradle | 4 +- .../com/maubis/scarlet/base/MainActivity.kt | 10 ++++ .../scarlet/base/core/note/NoteExtensions.kt | 1 + .../main/specs/MainActivityBottomBarSpec.kt | 23 +++++--- base/src/main/res/layout/activity_main.xml | 12 +++- base/src/main/res/values/strings.xml | 4 +- build.gradle | 4 +- scarlet/build.gradle | 2 +- .../quicknote/database/GDriveUploadData.kt | 3 - .../quicknote/drive/GDriveLoginActivity.kt | 22 +++++++- .../drive/GDriveLogoutActivitySpecs.kt | 10 ++++ .../quicknote/drive/GDriveRemoteDatabase.kt | 56 +++++++++++++------ .../drive/GDriveRemoteDatabaseState.kt | 46 +++++++++++---- .../quicknote/drive/GDriveRemoteFolder.kt | 11 +--- .../drive/GDriveRemoteImageFolder.kt | 6 +- .../quicknote/drive/GDriveServiceHelper.kt | 2 + .../quicknote/scarlet/ScarletAuthenticator.kt | 12 +--- 17 files changed, 153 insertions(+), 75 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bf6feb7e..5d1a5f44 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 131 - versionName '6.15.0' + versionCode 132 + versionName '7.0.1' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 5e3027f2..32a1bd3d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -161,6 +161,13 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { .setAdapter(adapter) .setLayoutManager(getLayoutManager(staggeredView, isTablet)) .build() + + vSwipeToRefresh.setOnRefreshListener { + when { + instance.authenticator().isLoggedIn(this) && !lastSyncHappening.get() -> instance.authenticator().requestSync(true) + else -> vSwipeToRefresh.isRefreshing = false + } + } } private fun getLayoutManager(isStaggeredView: Boolean, isTabletView: Boolean): RecyclerView.LayoutManager { @@ -388,6 +395,9 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { instance.authenticator().setPendingUploadListener(object : IPendingUploadListener { override fun onPendingSyncsUpdate(isSyncHappening: Boolean) { notifySyncingInformation(isSyncHappening, lastSyncPending.get()) + GlobalScope.launch(Dispatchers.Main) { + vSwipeToRefresh.isRefreshing = false + } } override fun onPendingStateUpdate(isDataSyncPending: Boolean) { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt index d516ace3..cbc42292 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt @@ -21,6 +21,7 @@ fun Note.isEqual(note: Note): Boolean { && this.color.toInt() == note.color.toInt() && this.locked == note.locked && this.pinned == note.pinned + && this.folder == note.folder } /************************************************************************************** diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 43d22fbb..61ac823d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -111,17 +111,24 @@ object MainActivityFolderBottomBarSpec { .flexGrow(1f) .text(folder.title) .textSizeRes(R.dimen.font_size_normal) - .textColor(colorConfig.toolbarIconColor)) + .textColor(colorConfig.toolbarIconColor) + .clickHandler(MainActivityFolderBottomBar.onClickEvent(context))) row.child(bottomBarRoundIcon(context, colorConfig) .iconRes(R.drawable.ic_more_options) - .onClick { - if (activity.config.folders.isEmpty()) { - return@onClick - } - CreateOrEditFolderBottomSheet.openSheet(activity, folder, { _, _ -> activity.setupData() }) - }) + .isClickDisabled(true) + .clickHandler(MainActivityFolderBottomBar.onClickEvent(context)) + .onClick {}) return bottomBarCard(context, row.build(), colorConfig).build() } + + @OnEvent(ClickEvent::class) + fun onClickEvent(context: ComponentContext, @Prop folder: Folder) { + val activity = context.androidContext as MainActivity + if (activity.config.folders.isEmpty()) { + return + } + CreateOrEditFolderBottomSheet.openSheet(activity, folder) { _, _ -> activity.setupData() } + } } @LayoutSpec @@ -208,13 +215,13 @@ object MainActivitySyncingNowSpec { .paddingDip(YogaEdge.VERTICAL, 8f) .paddingDip(YogaEdge.HORIZONTAL, 12f) .backgroundRes(R.drawable.login_button_disabled) + .clickHandler(MainActivitySyncingNow.onClickEvent(context)) .child(syncIcon) .child(Text.create(context) .typeface(FONT_MONSERRAT) .textRes(syncText) .textSizeRes(R.dimen.font_size_normal) .textColor(colorConfig.toolbarIconColor))) - .clickHandler(MainActivitySyncingNow.onClickEvent(context)) return row.build() } diff --git a/base/src/main/res/layout/activity_main.xml b/base/src/main/res/layout/activity_main.xml index 940a7e69..b7d43e93 100644 --- a/base/src/main/res/layout/activity_main.xml +++ b/base/src/main/res/layout/activity_main.xml @@ -20,11 +20,17 @@ android:layout_marginBottom="@dimen/spacing_xxsmall" android:visibility="gone" /> - + android:layout_weight="1"> + + + Signing In… Sign Out of Scarlet - Scarlet will stop syncing your notes, tags and folders to your Google Drive. + Stop syncing notes, tags and folders. + Stop syncing your notes, tags and folders to your Google Drive. + Your data on the app and on Google Drive will still be there. Sign Out from Drive Signing Out… diff --git a/build.gradle b/build.gradle index 6455df39..d50c26fb 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 131 - ext.appconfig_version = '6.15.0' + ext.appconfig_version_code = 132 + ext.appconfig_version = '7.0.1' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/build.gradle b/scarlet/build.gradle index 809ccfa3..f39c9548 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation ('com.google.http-client:google-http-client-gson:1.26.0') { - // exclude group: 'org.apache.httpcomponents' + exclude group: 'org.apache.httpcomponents' } implementation ('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt index 91e3922d..6049887a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt @@ -80,9 +80,6 @@ interface GDriveUploadDataDao { @Delete fun delete(note: GDriveUploadData) - @Query("DELETE FROM gdrive_upload WHERE 1") - fun drop() - @Query("SELECT * FROM gdrive_upload WHERE uid = :uid LIMIT 1") fun getByID(uid: Int): GDriveUploadData? diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index c693e89e..078e83fe 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -31,6 +31,7 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.maybeThrow +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference @@ -44,7 +45,6 @@ import java.util.concurrent.atomic.AtomicBoolean class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { private val RC_SIGN_IN = 31244 - private val RC_SIGN_IN_PERMISSIONS = 32443 lateinit var context: Context lateinit var component: Component @@ -61,6 +61,22 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed setButton(false) setupGoogleLogin() notifyThemeChange() + resetNotesToUploadStateDB() + } + + private fun resetNotesToUploadStateDB() { + GlobalScope.launch(Dispatchers.IO) { + val remoteDatabaseState = ApplicationBase.instance.remoteDatabaseState() as GDriveRemoteDatabaseState + ApplicationBase.instance.notesDatabase().getAll().forEach { + remoteDatabaseState.notifyInsertIfNotPresent(it) + } + ApplicationBase.instance.tagsDatabase().getAll().forEach { + remoteDatabaseState.notifyInsertIfNotPresent(it) + } + ApplicationBase.instance.foldersDatabase().getAll().forEach { + remoteDatabaseState.notifyInsertIfNotPresent(it) + } + } } private fun setupGoogleLogin() { @@ -139,7 +155,9 @@ class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailed gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) gDrive?.init(mDriveServiceHelper!!) - finish() + GlobalScope.launch(Dispatchers.IO) { + finish() + } } override fun onConnectionFailed(p0: ConnectionResult) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt index 14cc757a..2763620b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt @@ -76,6 +76,16 @@ object GDriveLogoutContentViewSpec { .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_logout_details) .typeface(CoreConfig.FONT_MONSERRAT)) + .child(GDriveIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.icon_sync_disabled) + .titleRes(R.string.google_drive_page_logout_no_sync_details)) + .child(GDriveIconView.create(context) + .marginDip(YogaEdge.TOP, 16f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_restore) + .titleRes(R.string.google_drive_page_logout_data_persists_details)) .build() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index e903d455..b05533a9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -55,6 +55,11 @@ var sGDriveFirstSyncImage: Boolean get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) ?: false set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) ?: Unit +const val KEY_G_DRIVE_LAST_FULL_SYNC_TIME = "g_drive_last_full_sync_time" +var sGDriveLastFullSyncTime: Long + get() = gDriveConfig?.get(KEY_G_DRIVE_LAST_FULL_SYNC_TIME, 0L) ?: 0L + set(value) = gDriveConfig?.put(KEY_G_DRIVE_LAST_FULL_SYNC_TIME, value) ?: Unit + fun folderIdForFolderName(folderName: String, folderId: String = ""): String { val key = "g_drive_folder_if_for_$folderName" if (folderId.isEmpty()) { @@ -157,23 +162,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { onPendingChange = { verifyAndNotifyPendingStateChange() }, onPendingSyncComplete = { action -> decrementPendingSyncs(action) }) - GlobalScope.launch { - val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) - when { - fuid.isNotBlank() -> onRootFolderLoaded(fuid) - else -> { - driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { - when { - (it === null) -> reset() - else -> { - folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER, it) - onRootFolderLoaded(it) - } - } - } - } - } - } + initRootFolder() } fun reset() { @@ -185,6 +174,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { imageSync = null } + fun logout() { GlobalScope.launch { reset() @@ -195,6 +185,28 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { /** * Initialisation Methods */ + + private fun initRootFolder() { + GlobalScope.launch { + sGDriveLastFullSyncTime = getTrueCurrentTime() + val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) + when { + fuid.isNotBlank() -> onRootFolderLoaded(fuid) + else -> { + driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { + when { + (it === null) -> reset() + else -> { + folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER, it) + onRootFolderLoaded(it) + } + } + } + } + } + } + } + private fun initSubRootFolder(folderName: String, folderId: String) { val logInfo = "initSubRootFolder($folderName, $folderId)" log("GDriveRemote", logInfo) @@ -348,11 +360,19 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { */ @Synchronized - fun resync() { + fun resync(forced: Boolean) { if (!isValidController) { return } + if (forced || (getTrueCurrentTime() - sGDriveLastFullSyncTime > 1000 * 60 * 60 * 24)) { + GlobalScope.launch { + gDriveDatabase?.resetAttempts() + initRootFolder() + } + return + } + GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) resyncDataSync(GDriveDataType.NOTE_META) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt index c86e97a0..7a0c90f8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt @@ -1,7 +1,10 @@ package com.bijoysingh.quicknote.drive import android.content.Context -import com.bijoysingh.quicknote.database.* +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveDatabaseHelper +import com.bijoysingh.quicknote.database.gDriveDatabase +import com.bijoysingh.quicknote.database.genGDriveUploadDatabase import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState @@ -31,6 +34,33 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { } } + fun notifyInsertIfNotPresent(data: Any) { + val database = gDriveDatabase + if (database === null) { + return + } + + when { + data is Tag -> { + if (database.getByUUID(GDriveDataType.TAG.name, data.uuid) === null) { + notifyInsert(data) {} + } + } + data is Folder -> { + if (database.getByUUID(GDriveDataType.FOLDER.name, data.uuid) === null) { + notifyInsert(data) {} + } + } + data is Note -> { + if (database.getByUUID(GDriveDataType.NOTE.name, data.uuid) === null + || database.getByUUID(GDriveDataType.NOTE_META.name, data.uuid) === null) { + notifyInsert(data) {} + } + } + else -> maybeThrow("notifyInsert called with unhandled data type") + } + } + private fun notifyNoteInsertImpl(note: Note, onExecution: () -> Unit) { val noteUuid = note.uuid localDatabaseUpdate(GDriveDataType.NOTE, noteUuid, onExecution) @@ -59,16 +89,8 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { imageUUIDs.forEach { val existing = database.getByUUID(GDriveDataType.IMAGE.name, it.name()) - if (existing !== null) { - return@launch - } - - GDriveUploadData().apply { - uuid = it.name() - type = GDriveDataType.IMAGE.name - lastUpdateTimestamp = getTrueCurrentTime() - localStateDeleted = false - save(database) + if (existing === null) { + localDatabaseUpdate(GDriveDataType.IMAGE, it.name(), {}, false) } } } @@ -152,7 +174,7 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { existing.apply { attempts = 0 lastAttemptTime = 0 - lastUpdateTimestamp = Math.max(gDriveUpdateTimestamp + 1, getTrueCurrentTime()) + lastUpdateTimestamp = Math.max(Math.max(gDriveUpdateTimestamp + 1, lastUpdateTimestamp + 1), getTrueCurrentTime()) localStateDeleted = removed save(database) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 45cf21e0..d2593c20 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -36,10 +36,7 @@ class GDriveRemoteFolder( contentLoading.set(true) contentFolderUid = fUid helper.getFilesInFolder(contentFolderUid).addOnCompleteListener { - if (it.result === null) { - // Something bad happened, probably network failure etc - networkOrAbsoluteFailure.set(true) - } + networkOrAbsoluteFailure.set(it.result === null) val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() @@ -79,10 +76,7 @@ class GDriveRemoteFolder( deletedLoading.set(true) deletedFolderUid = fUid helper.getFilesInFolder(deletedFolderUid).addOnCompleteListener { - if (it.result === null) { - // Something bad happened, probably network failure etc - networkOrAbsoluteFailure.set(true) - } + networkOrAbsoluteFailure.set(it.result === null) val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() @@ -140,6 +134,7 @@ class GDriveRemoteFolder( */ fun insert(uuid: String, item: T) { val logInfo = "insert($uuid)" + if (contentLoading.get()) { contentPendingActions.add(uuid) return diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 4c61ed19..0b7851d2 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -58,10 +58,7 @@ class GDriveRemoteImageFolder( contentFolderUid = fUid GlobalScope.launch(Dispatchers.IO) { helper.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { - if (it.result === null) { - // Something bad happened, probably network failure etc - networkOrAbsoluteFailure.set(true) - } + networkOrAbsoluteFailure.set(it.result === null) val imageFiles = it.result?.files if (imageFiles !== null) { @@ -85,6 +82,7 @@ class GDriveRemoteImageFolder( fun insert(id: ImageUUID) { val logInfo = "insert($id)" + if (contentLoading.get()) { contentPendingActions.add(id) return diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 5855bfe1..6c85548a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -192,6 +192,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { mDriveService.files().list() .setSpaces("drive") .setQ(query) + .setOrderBy("modifiedTime desc") .execute() }) } @@ -206,6 +207,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { mDriveService.files().list() .setSpaces("drive") .setQ("mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and ($nameQueryBuilder) and '$parentUid' in parents") + .setOrderBy("modifiedTime desc") .execute() }) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 69b3993a..e4024fc3 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -71,18 +71,8 @@ class ScarletAuthenticator() : IAuthenticator { } override fun requestSync(forced: Boolean) { - if (forced) { - GlobalScope.launch { - gDriveDatabase?.resetAttempts() - if (sGDriveLoggedIn) { - gDrive?.resync() - } - } - return - } - if (sGDriveLoggedIn) { - gDrive?.resync() + gDrive?.resync(forced) } } From af0658d97ce3a18add588232e028afc7928924f7 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 21 Jun 2019 23:37:00 +0100 Subject: [PATCH 041/134] [Gdrive] Fixing issues with deletion order --- app/build.gradle | 4 +- .../com/maubis/scarlet/base/MainActivity.kt | 4 +- .../sheet/SettingsOptionsBottomSheet.kt | 2 +- .../base/widget/AllNotesWidgetService.kt | 2 +- build.gradle | 4 +- scarlet/build.gradle | 2 +- .../quicknote/drive/GDriveAuthenticator.kt | 1 - .../quicknote/drive/GDriveRemoteFolder.kt | 49 ++++++++-- .../quicknote/drive/GDriveServiceHelper.kt | 92 ++++++++++++------- .../activity/FirebaseLoginActivity.kt | 2 + .../activity/FirebaseRemovalActivity.kt | 1 + .../quicknote/scarlet/ScarletAuthenticator.kt | 18 ++-- 12 files changed, 122 insertions(+), 59 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5d1a5f44..72c7feee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 132 - versionName '7.0.1' + versionCode 133 + versionName '7.0.2' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 32a1bd3d..0e085157 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -164,7 +164,9 @@ class MainActivity : ThemedActivity(), INoteOptionSheetActivity { vSwipeToRefresh.setOnRefreshListener { when { - instance.authenticator().isLoggedIn(this) && !lastSyncHappening.get() -> instance.authenticator().requestSync(true) + instance.authenticator().isLoggedIn(this) + && !instance.authenticator().isLegacyLoggedIn() + && !lastSyncHappening.get() -> instance.authenticator().requestSync(true) else -> vSwipeToRefresh.isRefreshing = false } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index 750430a2..c1f8b649 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -133,7 +133,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_sign_in_options, listener = { if (ApplicationBase.instance.authenticator().isLegacyLoggedIn()) { - ApplicationBase.instance.authenticator().logout() + ApplicationBase.instance.authenticator().openTransferDataActivity(activity)?.run() dismiss() return@LithoOptionsItem } diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt index cb3b8faf..1b24a47c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt @@ -34,7 +34,7 @@ class AllNotesRemoteViewsFactory(val context: Context) : RemoteViewsService.Remo } override fun getItemId(position: Int): Long { - return notes[position].uid.toLong() + return if(position >= notes.size) notes[position].uid.toLong() else 0 } override fun onDataSetChanged() { diff --git a/build.gradle b/build.gradle index d50c26fb..0aa35b5c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 132 - ext.appconfig_version = '7.0.1' + ext.appconfig_version_code = 133 + ext.appconfig_version = '7.0.2' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/build.gradle b/scarlet/build.gradle index f39c9548..dd760e0c 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation ('com.google.http-client:google-http-client-gson:1.26.0') { - exclude group: 'org.apache.httpcomponents' + exclude group: 'org.apache.httpcomponents' } implementation ('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt index 3c56e251..146ff1c6 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -17,7 +17,6 @@ class GDriveAuthenticator { var account: GoogleSignInAccount? = null fun setup(context: Context) { - gDriveDbState = GDriveRemoteDatabaseState(context) GlobalScope.launch { account = GoogleSignIn.getLastSignedInAccount(context) hasAccountSetup.set(true) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index d2593c20..56dc6cb7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -5,6 +5,7 @@ import com.bijoysingh.quicknote.database.GDriveUploadDataDao import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean class GDriveRemoteFolder( @@ -53,7 +54,10 @@ class GDriveRemoteFolder( contentLoading.set(false) GlobalScope.launch { executeAllDuplicateDeletion() } - GlobalScope.launch { executeInsertPendingActions() } + GlobalScope.launch { + executeInsertPendingActions() + executeDeletePendingActions() + } GlobalScope.launch { onLoaded() } }.addOnFailureListener { onPendingSyncComplete(logInfo) @@ -67,7 +71,10 @@ class GDriveRemoteFolder( val logInfo = "initDeletedFolderId($fUid)" if (fUid == INVALID_FILE_ID) { deletedLoading.set(false) - GlobalScope.launch { executeDeletePendingActions() } + GlobalScope.launch { + executeInsertPendingActions() + executeDeletePendingActions() + } GlobalScope.launch { onLoaded() } return } @@ -81,11 +88,13 @@ class GDriveRemoteFolder( val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() files.forEach { file -> - if (localFileIds.containsKey(file.name)) { - duplicateFilesToDelete.add(file.id) - } else { - localFileIds[file.name] = file.id - notifyDriveData(file, true) + when { + localFileIds.containsKey(file.name) -> duplicateFilesToDelete.add(file.id) + getTrueCurrentTime() - (file.modifiedTime?.value ?: 0L) > TimeUnit.DAYS.toMillis(7) -> duplicateFilesToDelete.add(file.id) + else -> { + localFileIds[file.name] = file.id + notifyDriveData(file, true) + } } } deletedFiles.clear() @@ -113,6 +122,9 @@ class GDriveRemoteFolder( } fun executeInsertPendingActions() { + if (deletedLoading.get() || contentLoading.get()) { + return + } contentPendingActions.forEach { uuid -> GlobalScope.launch { val item = uuidToObject(uuid) @@ -124,6 +136,9 @@ class GDriveRemoteFolder( } fun executeDeletePendingActions() { + if (deletedLoading.get() || contentLoading.get()) { + return + } deletedPendingActions.forEach { GlobalScope.launch { delete(it) } } @@ -135,8 +150,10 @@ class GDriveRemoteFolder( fun insert(uuid: String, item: T) { val logInfo = "insert($uuid)" - if (contentLoading.get()) { - contentPendingActions.add(uuid) + if (deletedLoading.get() || contentLoading.get()) { + if (!contentPendingActions.add(uuid)) { + onPendingSyncComplete(logInfo) + } return } @@ -145,6 +162,16 @@ class GDriveRemoteFolder( return } + if (deletedFiles.containsKey(uuid)) { + GlobalScope.launch { + val existingFileUid = deletedFiles[uuid] ?: INVALID_FILE_ID + helper.removeFileOrFolder(existingFileUid) + .addOnCompleteListener { + deletedFiles.remove(uuid) + } + } + } + val data = serialiser(item) val fileId = contentFiles[uuid] val existing = database.getByUUID(dataType.name, uuid) @@ -183,7 +210,9 @@ class GDriveRemoteFolder( fun delete(uuid: String) { val logInfo = "delete($uuid)" if (deletedLoading.get() || contentLoading.get()) { - deletedPendingActions.add(uuid) + if (!deletedPendingActions.add(uuid)) { + onPendingSyncComplete(logInfo) + } return } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 6c85548a..f0bef944 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -67,6 +67,8 @@ class ErrorCallable(val action: String, val callable: Callable) : Callable } catch (exception: InterruptedIOException) { // Ignore timeout exceptions return null + } catch (exception: ClassNotFoundException) { + return throwOrReturn(exception, null) } catch (exception: Exception) { return throwOrReturn(exception, null) } @@ -94,51 +96,67 @@ class GDriveServiceHelper(private val mDriveService: Drive) { log("GDrive", "createFileWithData($folderId, $name)") val contentToSave = if (content.isEmpty()) updateTime.toString() else content return execute("createFileWithData", Callable { - val metadata = File() - .setParents(listOf(folderId)) - .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) - .setModifiedTime(DateTime(updateTime)) - .setName(name) - val contentStream = ByteArrayContent.fromString("text/plain", contentToSave) - mDriveService.files().create(metadata, contentStream).execute() + try { + val metadata = File() + .setParents(listOf(folderId)) + .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) + .setModifiedTime(DateTime(updateTime)) + .setName(name) + val contentStream = ByteArrayContent.fromString("text/plain", contentToSave) + mDriveService.files().create(metadata, contentStream).execute() + } catch (exception: Exception) { + throwOrReturn(exception, null) + } }) } fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { log("GDrive", "createFileWithData($folderId, $name, ${file.absolutePath})") - return execute("createFileWithData", Callable { - val metadata = File() - .setParents(listOf(folderId)) - .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) - .setModifiedTime(DateTime(updateTime)) - .setName(name) - val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) - mDriveService.files().create(metadata, mediaContent).execute() + return execute("createFileWithData", Callable { + try { + val metadata = File() + .setParents(listOf(folderId)) + .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) + .setModifiedTime(DateTime(updateTime)) + .setName(name) + val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) + mDriveService.files().create(metadata, mediaContent).execute() + } catch (exception: Exception) { + throwOrReturn(exception, null) + } }) } fun createFolder(parentUid: String, folderName: String): Task { log("GDrive", "createFolder($parentUid, $folderName)") return execute("createFolder", Callable { - val timestamp = DateTime(getTrueCurrentTime()) - val metadata = File() - .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) - .setModifiedTime(timestamp) - .setName(folderName) - if (!parentUid.isEmpty()) { - metadata.parents = listOf(parentUid) + try { + val timestamp = DateTime(getTrueCurrentTime()) + val metadata = File() + .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) + .setModifiedTime(timestamp) + .setName(folderName) + if (!parentUid.isEmpty()) { + metadata.parents = listOf(parentUid) + } + mDriveService.files().create(metadata).execute() + } catch (exception: Exception) { + throwOrReturn(exception, null) } - mDriveService.files().create(metadata).execute() }) } fun readFile(fileId: String): Task { log("GDrive", "readFile($fileId)") return execute("readFile", Callable { - mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> - BufferedReader(InputStreamReader(`is`)).use { reader -> - reader.readText() + try { + mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> + BufferedReader(InputStreamReader(`is`)).use { reader -> + reader.readText() + } } + } catch (exception: Exception) { + throwOrReturn(exception, null) } }) } @@ -160,12 +178,16 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { log("GDrive", "saveFile($fileId, $name)") - return execute("saveFile", Callable { - val metadata = File() - .setModifiedTime(DateTime(updateTime)) - .setName(name) - val contentStream = ByteArrayContent.fromString("text/plain", content) - mDriveService.files().update(fileId, metadata, contentStream).execute() + return execute("saveFile", Callable { + try { + val metadata = File() + .setModifiedTime(DateTime(updateTime)) + .setName(name) + val contentStream = ByteArrayContent.fromString("text/plain", content) + mDriveService.files().update(fileId, metadata, contentStream).execute() + } catch (exception: Exception) { + throwOrReturn(exception, null) + } }) } @@ -215,7 +237,11 @@ class GDriveServiceHelper(private val mDriveService: Drive) { fun removeFileOrFolder(fileUid: String): Task { log("GDrive", "removeFileOrFolder($fileUid)") return execute("removeFileOrFolder", Callable { - mDriveService.files().delete(fileUid).execute() + try { + mDriveService.files().delete(fileUid).execute() + } catch (exception: Exception) { + maybeThrow(exception) + } null }) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt index 8cf4cc61..e7144630 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -7,6 +7,7 @@ import android.util.Log import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy import com.bijoysingh.quicknote.firebase.initFirebaseDatabase +import com.bijoysingh.quicknote.scarlet.sFirebaseKilled import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView @@ -64,6 +65,7 @@ class FirebaseLoginActivity : ThemedActivity() { } if (!loggingIn.get()) { setButton(true) + sFirebaseKilled = false signIn() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt index 6e61c7a9..e93a9501 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt @@ -8,6 +8,7 @@ import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy import com.bijoysingh.quicknote.firebase.initFirebaseDatabase import com.bijoysingh.quicknote.scarlet.sFirebaseKilled +import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index e4024fc3..76e562d2 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -8,6 +8,7 @@ import com.bijoysingh.quicknote.database.gDriveDatabase import com.bijoysingh.quicknote.drive.GDriveAuthenticator import com.bijoysingh.quicknote.drive.GDriveLoginActivity import com.bijoysingh.quicknote.drive.GDriveLogoutActivity +import com.bijoysingh.quicknote.drive.GDriveRemoteDatabaseState import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator @@ -31,14 +32,15 @@ class ScarletAuthenticator() : IAuthenticator { val gdrive = GDriveAuthenticator() override fun userId(context: Context): String? { - if (sGDriveLoggedIn) { + if (shouldIgnoreFirebase()) { return gdrive.userId(context) } return firebase.userId(context) } override fun setup(context: Context) { - if (sGDriveLoggedIn) { + Scarlet.gDriveDbState = GDriveRemoteDatabaseState(context) + if (shouldIgnoreFirebase()) { gdrive.setup(context) return } @@ -46,18 +48,18 @@ class ScarletAuthenticator() : IAuthenticator { } override fun isLoggedIn(context: Context): Boolean { - if (sGDriveLoggedIn) { + if (shouldIgnoreFirebase()) { return gdrive.isLoggedIn(context) } return firebase.isLoggedIn() } override fun isLegacyLoggedIn(): Boolean { - return firebase.isLoggedIn() + return !shouldIgnoreFirebase() && firebase.isLoggedIn() } override fun logout() { - if (sGDriveLoggedIn) { + if (shouldIgnoreFirebase()) { gdrive.logout() return } @@ -65,17 +67,19 @@ class ScarletAuthenticator() : IAuthenticator { } override fun setPendingUploadListener(listener: IPendingUploadListener?) { - if (sGDriveLoggedIn) { + if (shouldIgnoreFirebase()) { gDrive?.setPendingUploadListener(listener) } } override fun requestSync(forced: Boolean) { - if (sGDriveLoggedIn) { + if (shouldIgnoreFirebase()) { gDrive?.resync(forced) } } + private fun shouldIgnoreFirebase() = sFirebaseKilled || sGDriveLoggedIn + override fun openLoginActivity(context: Context) = Runnable { context.startActivity(Intent(context, GDriveLoginActivity::class.java)) } From 75561cf81f122d24876baf3182e6a81b7223355f Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 23 Jun 2019 01:03:49 +0100 Subject: [PATCH 042/134] Adding full screen internal settings for screenshots --- app/build.gradle | 4 +-- .../scarlet/base/note/NoteExtensions.kt | 3 +- .../creation/activity/CreateNoteActivity.kt | 1 + .../InternalSettingsOptionsBottomSheet.kt | 33 +++++++++++++++++++ .../scarlet/base/support/ui/ThemedActivity.kt | 24 ++++++++++++++ base/src/main/res/values/strings.xml | 6 ++++ build.gradle | 4 +-- scarlet/build.gradle | 1 + 8 files changed, 71 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 72c7feee..cbb60d65 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 133 - versionName '7.0.2' + versionCode 134 + versionName '7.0.3' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 5e97bb6c..1273ee3c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -19,6 +19,7 @@ import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_DISTRACTION_FREE import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity +import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.removeMarkdownHeaders @@ -88,7 +89,7 @@ fun Note.getText(): String { } val text = stringBuilder.toString().trim() - if (BuildConfig.DEBUG) { + if (sInternalShowUUID) { return "`$uuid`\n\n$text" } return text diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index 3ede9d86..0158b1eb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -227,6 +227,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { override fun run() { if (active) { maybeUpdateNoteWithoutSync() + fullScreenView() handler.postDelayed(this, HANDLER_UPDATE_TIME.toLong()) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt index 284fb0f3..9ca243db 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt @@ -4,16 +4,49 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.utils.* +const val KEY_INTERNAL_ENABLE_FULL_SCREEN = "internal_enable_full_screen" +var sInternalEnableFullScreen: Boolean + get() = ApplicationBase.instance.store().get(KEY_INTERNAL_ENABLE_FULL_SCREEN, false) + set(value) = ApplicationBase.instance.store().put(KEY_INTERNAL_ENABLE_FULL_SCREEN, value) + +const val KEY_INTERNAL_SHOW_UUID = "internal_show_uuid" +var sInternalShowUUID: Boolean + get() = ApplicationBase.instance.store().get(KEY_INTERNAL_SHOW_UUID, false) + set(value) = ApplicationBase.instance.store().put(KEY_INTERNAL_SHOW_UUID, value) + class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.internal_settings_title override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = context as MainActivity val options = ArrayList() + options.add(LithoOptionsItem( + title = R.string.internal_settings_enable_fullscreen_title, + subtitle = R.string.internal_settings_enable_fullscreen_description, + icon = R.drawable.ic_action_grid, + listener = { + sInternalEnableFullScreen = !sInternalEnableFullScreen + reset(activity, dialog) + }, + isSelectable = true, + selected = sInternalEnableFullScreen + )) + options.add(LithoOptionsItem( + title = R.string.internal_settings_show_uuid_title, + subtitle = R.string.internal_settings_show_uuid_description, + icon = R.drawable.ic_code_white_48dp, + listener = { + sInternalShowUUID = !sInternalShowUUID + reset(activity, dialog) + }, + isSelectable = true, + selected = sInternalShowUUID + )) options.add(LithoOptionsItem( title = R.string.internal_settings_enable_log_exceptions_title, subtitle = R.string.internal_settings_enable_log_exceptions_description, diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index 238ef3e3..95d01670 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -5,7 +5,9 @@ import android.os.Build import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager +import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.settings.sheet.sInternalEnableFullScreen import com.maubis.scarlet.base.support.utils.maybeThrow abstract class ThemedActivity : AppCompatActivity() { @@ -17,6 +19,28 @@ abstract class ThemedActivity : AppCompatActivity() { setStatusBarTextColor() } + override fun onResume() { + super.onResume() + fullScreenView() + } + + fun fullScreenView() { + if (!sInternalEnableFullScreen) { + return + } + + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_IMMERSIVE + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN) + } + fun setStatusBarColor(color: Int) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window.statusBarColor = color diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9ebb4ea6..dc0b7302 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -446,6 +446,12 @@ Throw on Exceptions Throw and crash the application on exceptions. Will be reset after 5 crashes + Enable Fullscreen + Make the app in fullscreen to allow screenshots and recordings + + Show Note UUIDs + Show the unique ids of the notes in the home screen view + Fake Exceptions Throw a fake exception to test the exception features diff --git a/build.gradle b/build.gradle index 0aa35b5c..677af169 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 133 - ext.appconfig_version = '7.0.2' + ext.appconfig_version_code = 134 + ext.appconfig_version = '7.0.3' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/build.gradle b/scarlet/build.gradle index dd760e0c..ef38cf22 100644 --- a/scarlet/build.gradle +++ b/scarlet/build.gradle @@ -73,6 +73,7 @@ dependencies { kapt "com.facebook.litho:litho-processor:$litho_version" implementation 'com.google.android.gms:play-services-auth:16.0.1' + implementation 'commons-codec:commons-codec:1.10' implementation ('com.google.http-client:google-http-client-gson:1.26.0') { exclude group: 'org.apache.httpcomponents' } From 65623a7179908b5fa10608d6f821bf446f432fe5 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 27 Jul 2019 00:20:01 +0100 Subject: [PATCH 043/134] Making functions for text/markdown more descriptive --- .../base/core/note/MaterialNoteActor.kt | 4 +- .../base/core/note/NoteSortingUtils.kt | 6 +- .../scarlet/base/note/NoteExtensions.kt | 103 ++++++++---------- .../note/actions/TextToSpeechBottomSheet.kt | 14 ++- .../base/note/recycler/NoteRecyclerItem.kt | 8 +- .../recycler/NoteRecyclerViewHolderBase.kt | 9 -- .../base/notification/NotificationHandler.kt | 12 +- .../base/service/FloatingNoteService.kt | 17 +-- base/src/main/res/layout/item_note.xml | 15 --- .../main/res/layout/item_note_staggered.xml | 15 --- .../res/layout/layout_add_note_overlay.xml | 15 --- .../main/java/com/maubis/markdown/Markdown.kt | 16 ++- .../markdown/spannable/SpannableExtensions.kt | 4 +- 13 files changed, 91 insertions(+), 147 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index bbe3e918..24ee2cd3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -32,8 +32,8 @@ open class MaterialNoteActor(val note: Note) : INoteActor { override fun share(context: Context) { IntentUtils.ShareBuilder(context) - .setSubject(note.getTitle()) - .setText(note.getText()) + .setSubject(note.getTitleForSharing()) + .setText(note.getFullText()) .setChooserText(context.getString(R.string.share_using)) .share() } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt index dab38cdd..fd932642 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.core.note import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.getAlphabets +import com.maubis.scarlet.base.note.getFullText enum class SortingTechnique() { LAST_MODIFIED, @@ -22,7 +22,9 @@ fun sort(notes: List, sortingTechnique: SortingTechnique): List { else note.timestamp } SortingTechnique.ALPHABETICAL -> notes.sortedBy { note -> - val content = note.getAlphabets() + val content = note.getFullText().trim().filter { + ((it in 'a'..'z') || (it in 'A'..'Z')) + } if (note.pinned || content.isBlank()) 0 else content[0].toUpperCase().toInt() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 1273ee3c..63f84ce4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -4,8 +4,9 @@ import android.content.Context import android.content.Intent import com.github.bijoysingh.starter.util.DateFormatter import com.google.gson.Gson -import com.maubis.markdown.BuildConfig import com.maubis.markdown.Markdown +import com.maubis.markdown.MarkdownConfig +import com.maubis.markdown.spannable.* import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -22,32 +23,17 @@ import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.utils.removeMarkdownHeaders import java.util.* import kotlin.collections.ArrayList -fun Note.log(context: Context): String { - val log = HashMap() - log["note"] = this - log["_title"] = getTitle() - log["_text"] = getText() - log["_image"] = getImageFile() - log["_locked"] = getLockedText(false) - log["_fullText"] = getFullText() - log["_displayTime"] = getDisplayTime() - log["_tag"] = getTagString() - log["_formats"] = getFormats() - return Gson().toJson(log) -} - fun Note.log(): String { val log = HashMap() log["note"] = this - log["_title"] = getTitle() - log["_text"] = getText() + log["_text"] = getFullText() log["_image"] = getImageFile() log["_fullText"] = getFullText() log["_displayTime"] = getDisplayTime() + log["_tag"] = getTagString() log["_formats"] = getFormats() return Gson().toJson(log) } @@ -55,7 +41,47 @@ fun Note.log(): String { /************************************************************************************** ************* Content and Display Information Functions Functions ******************** **************************************************************************************/ -fun Note.getTitle(): String { + +fun Note.getFullTextForDirectMarkdownRender(): String { + var text = getFullText() + text = text.replace("\n[x] ", "\n\u2611 ") + text = text.replace( "\n[ ] ", "\n\u2610 ") + text = text.replace( "\n- ", "\n\u2022 ") + return text +} + +fun Note.getMarkdownForListView(isMarkdownEnabled: Boolean): CharSequence { + var text = getFullTextForDirectMarkdownRender() + return when { + isMarkdownEnabled -> Markdown.renderWithCustomFormatting(text, true) { spannable, spanInfo -> + val s = spanInfo.start + val e = spanInfo.end + when (spanInfo.markdownType) { + MarkdownType.HEADING_1 -> { + spannable.relativeSize(1.2f, s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) + true + } + MarkdownType.HEADING_2 -> { + spannable.relativeSize(1.1f, s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) + true + } + MarkdownType.CHECKLIST_CHECKED -> { + spannable.strike(s, e) + true + } + else -> false + } + } + else -> text + } +} + + +fun Note.getTitleForSharing(): String { val formats = getFormats() if (formats.isEmpty()) { return "" @@ -68,7 +94,7 @@ fun Note.getTitle(): String { } } -fun Note.getText(): String { +fun Note.getTextForSharing(): String { val formats = getFormats().toMutableList() if (formats.isEmpty()) { return "" @@ -120,47 +146,14 @@ fun Note.getImageFile(): String { return format?.text ?: "" } -fun Note.getMarkdownTitle(isMarkdownEnabled: Boolean): CharSequence { - val titleString = getTitle() - return when { - titleString.isBlank() -> "" - !isMarkdownEnabled -> Markdown.render(removeMarkdownHeaders(titleString), true) - else -> titleString - } -} - -fun Note.getMarkdownText(isMarkdownEnabled: Boolean): CharSequence { - return when { - isMarkdownEnabled -> Markdown.render(removeMarkdownHeaders(getText()), true) - else -> getText() - } -} - fun Note.getFullText(): String { return getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim() } -fun Note.getAlphabets(): String { - return getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim().filter { - ((it in 'a'..'z') || (it in 'A'..'Z')) - } -} - -fun Note.getImageIds(): Set { - return getFormats().filter { it.formatType == FormatType.IMAGE }.map { it.text }.toSet() -} - -fun Note.getUnreliablyStrippedText(context: Context): String { - val builder = StringBuilder() - builder.append(Markdown.render(removeMarkdownHeaders(getTitle())), true) - builder.append(Markdown.render(removeMarkdownHeaders(getText())), true) - return builder.toString().trim { it <= ' ' } -} - -fun Note.getLockedText(isMarkdownEnabled: Boolean): CharSequence { +fun Note.getLockedAwareTextForHomeList(isMarkdownEnabled: Boolean): CharSequence { val text = when { this.locked -> "******************\n***********\n****************" - else -> getMarkdownText(isMarkdownEnabled) + else -> getMarkdownForListView(isMarkdownEnabled) } return text diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index 259d5cd8..a4844e08 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -5,13 +5,21 @@ import android.os.Build import android.speech.tts.TextToSpeech import android.widget.ImageView import android.widget.TextView +import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.getUnreliablyStrippedText +import com.maubis.scarlet.base.note.getFullText import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment +import com.maubis.scarlet.base.support.utils.removeMarkdownHeaders + +fun Note.getTextToSpeechText(): String { + val builder = StringBuilder() + builder.append(Markdown.render(removeMarkdownHeaders(getFullText())), true) + return builder.toString().trim { it <= ' ' } +} class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { @@ -57,9 +65,9 @@ class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { fun speak(note: Note) { if (Build.VERSION.SDK_INT >= 21) { - textToSpeech?.speak(note.getUnreliablyStrippedText(themedContext()), TextToSpeech.QUEUE_FLUSH, null, "NOTE") + textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null, "NOTE") } else { - textToSpeech?.speak(note.getUnreliablyStrippedText(themedContext()), TextToSpeech.QUEUE_FLUSH, null) + textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt index 960a5af4..170fdc84 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt @@ -20,13 +20,7 @@ class NoteRecyclerItem(context: Context, val note: Note) : RecyclerItem() { private val isMarkdownEnabled = sEditorMarkdownEnabled && sMarkdownEnabledHome val lineCount = sNoteItemLineCount - val title = note.getMarkdownTitle(isMarkdownEnabled) - val titleColor = when (isLightShaded) { - true -> ContextCompat.getColor(context, R.color.dark_tertiary_text) - false -> ContextCompat.getColor(context, R.color.light_primary_text) - } - - val description = note.getLockedText(isMarkdownEnabled) + val description = note.getLockedAwareTextForHomeList(isMarkdownEnabled) val descriptionColor = when (isLightShaded) { true -> ContextCompat.getColor(context, R.color.dark_tertiary_text) false -> ContextCompat.getColor(context, R.color.light_primary_text) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt index 8408918d..cbabf065 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt @@ -23,7 +23,6 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi protected val view: CardView protected val tags: TextView protected val image: ImageView - protected val title: TextView protected val description: TextView protected val edit: ImageView protected val share: ImageView @@ -41,7 +40,6 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi this.view = view as CardView tags = view.findViewById(R.id.tags) image = view.findViewById(R.id.image) - title = view.findViewById(R.id.title) description = view.findViewById(R.id.description) share = view.findViewById(R.id.share_button) delete = view.findViewById(R.id.delete_button) @@ -57,7 +55,6 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi override fun populate(itemData: RecyclerItem, extra: Bundle?) { val item = itemData as NoteRecyclerItem - setTitle(item) setDescription(item) setImage(item) setIndicators(item) @@ -72,12 +69,6 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi setActionBar(item, extra) } - private fun setTitle(note: NoteRecyclerItem) { - title.text = note.title - title.visibility = if (note.title.isEmpty()) View.GONE else View.VISIBLE - title.setTextColor(note.titleColor) - } - private fun setDescription(note: NoteRecyclerItem) { description.text = note.description description.maxLines = note.lineCount diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt index 18c54672..8bd514bc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt @@ -18,8 +18,8 @@ import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.getDisplayTime -import com.maubis.scarlet.base.note.getText -import com.maubis.scarlet.base.note.getTitle +import com.maubis.scarlet.base.note.getTextForSharing +import com.maubis.scarlet.base.note.getTitleForSharing import com.maubis.scarlet.base.support.INTENT_KEY_ACTION import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -46,7 +46,7 @@ class NotificationHandler(val context: Context) { var contentView = getRemoteView(config) val notificationBuilder = NotificationCompat.Builder(context, config.channel) .setSmallIcon(R.drawable.ic_format_quote_white_48dp) - .setContentTitle(config.note.getTitle()) + .setContentTitle(config.note.getTitleForSharing()) .setColor(config.note.color) .setCategory(NotificationCompat.CATEGORY_EVENT) .setContent(contentView) @@ -87,10 +87,10 @@ class NotificationHandler(val context: Context) { fun getRemoteView(config: NotificationConfig): RemoteViews { val contentView = RemoteViews(context.packageName, R.layout.notification_note_layout) - val hasTitle = !TextUtils.isNullOrEmpty(config.note.getTitle()) + val hasTitle = !TextUtils.isNullOrEmpty(config.note.getTitleForSharing()) contentView.setViewVisibility(R.id.title, if (hasTitle) VISIBLE else GONE) - contentView.setTextViewText(R.id.title, config.note.getTitle()) - contentView.setTextViewText(R.id.description, config.note.getText()) + contentView.setTextViewText(R.id.title, config.note.getTitleForSharing()) + contentView.setTextViewText(R.id.description, config.note.getTextForSharing()) contentView.setTextViewText(R.id.timestamp, config.note.getDisplayTime()) val theme = ApplicationBase.instance.themeController() diff --git a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt index 02ae78c3..b091a37c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt @@ -11,7 +11,7 @@ import android.widget.TextView import com.bsk.floatingbubblelib.FloatingBubbleConfig import com.bsk.floatingbubblelib.FloatingBubblePermissions import com.bsk.floatingbubblelib.FloatingBubbleService -import com.github.bijoysingh.starter.util.TextUtils +import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb @@ -31,7 +31,6 @@ import com.maubis.scarlet.base.support.utils.maybeThrow class FloatingNoteService : FloatingBubbleService() { private var note: Note? = null - private lateinit var title: TextView private lateinit var description: TextView private lateinit var timestamp: TextView private lateinit var panel: View @@ -73,11 +72,9 @@ class FloatingNoteService : FloatingBubbleService() { val theme = ApplicationBase.instance.themeController() val rootView = getInflater().inflate(R.layout.layout_add_note_overlay, null) - title = rootView.findViewById(R.id.title) as TextView description = rootView.findViewById(R.id.description) as TextView timestamp = rootView.findViewById(R.id.timestamp) as TextView - title.setTextColor(theme.get(ThemeColorType.SECONDARY_TEXT)) description.setTextColor(theme.get(ThemeColorType.SECONDARY_TEXT)) val noteItem = note!! @@ -118,22 +115,18 @@ class FloatingNoteService : FloatingBubbleService() { } fun getShareIntent(note: Note) { - val sharingIntent = Intent(android.content.Intent.ACTION_SEND) + val sharingIntent = Intent(Intent.ACTION_SEND) sharingIntent.type = "text/plain" - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, note.getTitle()) - sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, note.getText()) + sharingIntent.putExtra(Intent.EXTRA_SUBJECT, note.getTitleForSharing()) + sharingIntent.putExtra(Intent.EXTRA_TEXT, note.getTextForSharing()) sharingIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) context.startActivity(sharingIntent) } fun setNote(note: Note) { - val noteTitle = note.getTitle() - val noteDescription = note.getMarkdownText(true) - title.text = noteTitle + val noteDescription = Markdown.render(note.getFullTextForDirectMarkdownRender(), true) description.text = noteDescription timestamp.text = note.getDisplayTime() - - title.visibility = if (TextUtils.isNullOrEmpty(noteTitle)) View.GONE else View.VISIBLE } companion object { diff --git a/base/src/main/res/layout/item_note.xml b/base/src/main/res/layout/item_note.xml index 9fb9ac9e..5c488354 100644 --- a/base/src/main/res/layout/item_note.xml +++ b/base/src/main/res/layout/item_note.xml @@ -32,21 +32,6 @@ android:layout_weight="1" android:orientation="vertical"> - - - - - - Boolean): Spannable { + val spans = getSpanInfo(text, strip) + val spannable = SpannableStringBuilder(spans.text) + spans.spans.forEach { + if (!customSpanInfoAction(spannable, it)) { + spannable.setDefaultFormats(it) + } + } + return spannable + } + fun renderSegment(text: String, strip: Boolean = false): Spannable { val inliner = TextInliner(text).get() val strippedText = inliner.contentText(strip) diff --git a/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt b/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt index c1c0c24e..f9a50920 100644 --- a/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt +++ b/markdown/src/main/java/com/maubis/markdown/spannable/SpannableExtensions.kt @@ -90,7 +90,7 @@ fun Spannable.font(font: Typeface, start: Int, end: Int): Spannable { return this } -fun Spannable.setFormats(info: SpanInfo) { +fun Spannable.setDefaultFormats(info: SpanInfo) { val s = info.start val e = info.end when (info.markdownType) { @@ -120,5 +120,5 @@ fun Spannable.setFormats(info: SpanInfo) { } fun Spannable.setFormats(info: List) { - info.forEach { setFormats(it) } + info.forEach { setDefaultFormats(it) } } \ No newline at end of file From 28fc343877c9aa370b179c6c4c1499da55d01f20 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Jul 2019 14:35:58 +0100 Subject: [PATCH 044/134] Prioritising deletion of notes over changes --- app/build.gradle | 4 ++-- .../com/maubis/scarlet/base/note/NoteExtensions.kt | 11 ++++++++--- .../base/widget/sheet/WidgetOptionsBottomSheet.kt | 3 ++- base/src/main/res/layout/layout_add_note_overlay.xml | 1 + build.gradle | 4 ++-- .../quicknote/drive/GDriveRemoteDatabase.kt | 5 +++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cbb60d65..d9b70ebd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 134 - versionName '7.0.3' + versionCode 135 + versionName '7.0.4' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 63f84ce4..d259cca2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -7,6 +7,7 @@ import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.MarkdownConfig import com.maubis.markdown.spannable.* +import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -45,8 +46,8 @@ fun Note.log(): String { fun Note.getFullTextForDirectMarkdownRender(): String { var text = getFullText() text = text.replace("\n[x] ", "\n\u2611 ") - text = text.replace( "\n[ ] ", "\n\u2610 ") - text = text.replace( "\n- ", "\n\u2022 ") + text = text.replace("\n[ ] ", "\n\u2610 ") + text = text.replace("\n- ", "\n\u2022 ") return text } @@ -147,7 +148,11 @@ fun Note.getImageFile(): String { } fun Note.getFullText(): String { - return getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim() + val fullText = getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim() + if (BuildConfig.DEBUG) { + return "`$uuid`\n$fullText" + } + return fullText } fun Note.getLockedAwareTextForHomeList(isMarkdownEnabled: Boolean): CharSequence { diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index 4d5a43e7..95a5303f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -15,6 +15,7 @@ import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.getFullText +import com.maubis.scarlet.base.note.getFullTextForDirectMarkdownRender import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem @@ -49,7 +50,7 @@ fun getWidgetNoteText(note: Note): CharSequence { return "******************\n***********\n****************" } - val text = note.getFullText() + val text = note.getFullTextForDirectMarkdownRender() return when (sWidgetEnableFormatting) { true -> Markdown.render(text, true) false -> text diff --git a/base/src/main/res/layout/layout_add_note_overlay.xml b/base/src/main/res/layout/layout_add_note_overlay.xml index 12d2ebca..3b47ee57 100644 --- a/base/src/main/res/layout/layout_add_note_overlay.xml +++ b/base/src/main/res/layout/layout_add_note_overlay.xml @@ -17,6 +17,7 @@ android:background="@android:color/transparent" android:gravity="top" android:hint="@string/description" + android:lineHeight="21sp" android:lineSpacingExtra="6dp" android:padding="@dimen/spacing_normal" android:textColor="@color/dark_secondary_text" diff --git a/build.gradle b/build.gradle index 677af169..904ccbe9 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 134 - ext.appconfig_version = '7.0.3' + ext.appconfig_version_code = 135 + ext.appconfig_version = '7.0.4' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index b05533a9..b7aafa3d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -396,15 +396,16 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { log("GDrive", "resyncDataSync(${type.name}, ${pendingItem.uuid}, ${pendingItem.lastUpdateTimestamp}, ${pendingItem.gDriveUpdateTimestamp})") val sameDelete = pendingItem.localStateDeleted == pendingItem.gDriveStateDeleted val localDeleted = pendingItem.localStateDeleted + val remoteDeleted = pendingItem.gDriveStateDeleted val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.gDriveUpdateTimestamp if (!sameUpdateTime || !sameUpdateTime) { when { + !sameDelete && remoteDeleted && !sameUpdateTime -> onRemoteRemove(type, pendingItem) // Remote removal more takes precedence to local updates + !sameDelete && localDeleted && !sameUpdateTime -> remove(type, pendingItem) // Local removal more takes precedence to remote updates sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp && localDeleted -> remove(type, pendingItem) sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> insert(type, pendingItem) sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp && localDeleted -> onRemoteRemove(type, pendingItem) sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteInsert(type, pendingItem) - !sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> remove(type, pendingItem) - !sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteRemove(type, pendingItem) !sameDelete && sameUpdateTime -> gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) // Ignoring } } From 4ec8693e51e7e1dab8fc0b62dd6e0ae107f4f895 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Jul 2019 17:42:56 +0100 Subject: [PATCH 045/134] Restructuring settings to allow more PIN options --- .../main/specs/MainActivityBottomBarSpec.kt | 2 +- .../scarlet/base/note/NoteExtensions.kt | 5 +- .../creation/activity/ViewNoteActivity.kt | 2 +- .../sheet/EditorOptionsBottomSheet.kt | 27 ++++++++++ .../formats/recycler/FormatViewHolderBase.kt | 2 +- .../sheet/NoteSettingsOptionsBottomSheet.kt | 50 ------------------- .../sheet/SecurityOptionsBottomSheet.kt | 1 + .../sheet/SettingsOptionsBottomSheet.kt | 17 ++++--- .../quicknote/drive/GDriveRemoteDatabase.kt | 4 +- 9 files changed, 46 insertions(+), 64 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 61ac823d..6245d9db 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -21,8 +21,8 @@ import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.main.sheets.HomeOptionsBottomSheet import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity +import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.note.folder.sheet.CreateOrEditFolderBottomSheet -import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.* import com.maubis.scarlet.base.support.ui.ColorUtil diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index d259cca2..f4987724 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -21,8 +21,8 @@ import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_DISTRACTION_FREE import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity +import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID -import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.ui.ThemedActivity import java.util.* import kotlin.collections.ArrayList @@ -53,6 +53,9 @@ fun Note.getFullTextForDirectMarkdownRender(): String { fun Note.getMarkdownForListView(isMarkdownEnabled: Boolean): CharSequence { var text = getFullTextForDirectMarkdownRender() + if (sInternalShowUUID) { + text = "`$uuid`\n\n$text" + } return when { isMarkdownEnabled -> Markdown.renderWithCustomFormatting(text, true) { spannable, spanInfo -> val s = spanInfo.start diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 9f2e35f4..01fb1abf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -23,6 +23,7 @@ import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.actions.NoteOptionsBottomSheet import com.maubis.scarlet.base.note.activity.INoteOptionSheetActivity +import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.note.creation.specs.NoteViewBottomBar import com.maubis.scarlet.base.note.creation.specs.NoteViewTopBar import com.maubis.scarlet.base.note.formats.FormatAdapter @@ -34,7 +35,6 @@ import com.maubis.scarlet.base.settings.sheet.STORE_KEY_TEXT_SIZE import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_ENABLED import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.useNoteColorAsBackground import com.maubis.scarlet.base.settings.sheet.sEditorTextSize -import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.ui.* import com.maubis.scarlet.base.support.ui.ColorUtil.darkerColor diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt index fca132f9..b982a9a2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt @@ -2,16 +2,21 @@ package com.maubis.scarlet.base.note.creation.sheet import android.app.Dialog import com.facebook.litho.ComponentContext +import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet +import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.openSheet const val STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED = "KEY_MARKDOWN_ENABLED" const val STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN = "editor_live_markdown" const val STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS = "editor_move_checked_items" const val STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT = "editor_markdown_default" const val STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES = "editor_move_handles" +const val STORE_KEY_NOTE_DEFAULT_COLOR = "KEY_NOTE_DEFAULT_COLOR" var sEditorLiveMarkdown: Boolean get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, true) @@ -33,12 +38,34 @@ var sEditorMarkdownEnabled: Boolean get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, true) set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, value) +var sNoteDefaultColor: Int + get() = ApplicationBase.instance.store().get(STORE_KEY_NOTE_DEFAULT_COLOR, (0xFFD32F2F).toInt()) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_NOTE_DEFAULT_COLOR, value) + class EditorOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.home_option_editor_options_title override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val items = ArrayList() + val activity = context as MainActivity + items.add(LithoOptionsItem( + title = R.string.note_option_default_color, + subtitle = R.string.note_option_default_color_subtitle, + icon = R.drawable.ic_action_color, + listener = { + val config = ColorPickerDefaultController( + title = R.string.note_option_default_color, + colors = listOf( + activity.resources.getIntArray(R.array.bright_colors), + activity.resources.getIntArray(R.array.bright_colors_accent)), + selectedColor = sNoteDefaultColor, + onColorSelected = { sNoteDefaultColor = it } + ) + openSheet(activity, ColorPickerBottomSheet().apply { this.config = config }) + dismiss() + } + )) items.add(LithoOptionsItem( title = R.string.markdown_sheet_markdown_support, subtitle = R.string.markdown_sheet_markdown_support_subtitle, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index e6eb8bab..7b1bfb54 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -11,11 +11,11 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity +import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.settings.sheet.STORE_KEY_TEXT_SIZE import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.TEXT_SIZE_DEFAULT import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.useNoteColorAsBackground -import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.support.ui.Theme import com.maubis.scarlet.base.support.ui.ThemeColorType diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt deleted file mode 100644 index 3efbb3a2..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/NoteSettingsOptionsBottomSheet.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.maubis.scarlet.base.settings.sheet - -import android.app.Dialog -import com.facebook.litho.ComponentContext -import com.maubis.scarlet.base.MainActivity -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet -import com.maubis.scarlet.base.support.sheets.LithoOptionsItem -import com.maubis.scarlet.base.support.sheets.openSheet - -const val STORE_KEY_NOTE_DEFAULT_COLOR = "KEY_NOTE_DEFAULT_COLOR" - -var sNoteDefaultColor: Int - get() = ApplicationBase.instance.store().get(STORE_KEY_NOTE_DEFAULT_COLOR, (0xFFD32F2F).toInt()) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_NOTE_DEFAULT_COLOR, value) - -class NoteSettingsOptionsBottomSheet : LithoOptionBottomSheet() { - override fun title(): Int = R.string.home_option_note_settings - - override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { - val activity = context as MainActivity - val options = ArrayList() - options.add(LithoOptionsItem( - title = R.string.note_option_default_color, - subtitle = R.string.note_option_default_color_subtitle, - icon = R.drawable.ic_action_color, - listener = { - val config = ColorPickerDefaultController( - title = R.string.note_option_default_color, - colors = listOf(activity.resources.getIntArray(R.array.bright_colors), activity.resources.getIntArray(R.array.bright_colors_accent)), - selectedColor = sNoteDefaultColor, - onColorSelected = { sNoteDefaultColor = it } - ) - openSheet(activity, ColorPickerBottomSheet().apply { this.config = config }) - dismiss() - } - )) - options.add(LithoOptionsItem( - title = R.string.home_option_security, - subtitle = R.string.home_option_security_subtitle, - icon = R.drawable.ic_option_security, - listener = { - SecurityOptionsBottomSheet.openSheet(activity) - dismiss() - } - )) - return options - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index b4d954b1..9a4f1cb7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -35,6 +35,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { isSelectable = true, selected = !TextUtils.isNullOrEmpty(ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "")) )) + val hasFingerprint = Reprint.hasFingerprintRegistered() options.add(LithoOptionsItem( title = R.string.security_option_fingerprint_enabled, diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index c1f8b649..8897b43f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -60,14 +60,6 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { UISettingsOptionsBottomSheet.openSheet(activity) } )) - options.add(LithoOptionsItem( - title = R.string.home_option_note_settings, - subtitle = R.string.home_option_note_settings_subtitle, - icon = R.drawable.ic_subject_white_48dp, - listener = { - openSheet(activity, NoteSettingsOptionsBottomSheet()) - } - )) options.add(LithoOptionsItem( title = R.string.home_option_editor_options_title, subtitle = R.string.home_option_editor_options_description, @@ -84,6 +76,15 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { openSheet(activity, BackupSettingsOptionsBottomSheet()) } )) + options.add(LithoOptionsItem( + title = R.string.home_option_security, + subtitle = R.string.home_option_security_subtitle, + icon = R.drawable.ic_option_security, + listener = { + SecurityOptionsBottomSheet.openSheet(activity) + dismiss() + } + )) options.add(LithoOptionsItem( title = R.string.home_option_widget_options_title, subtitle = R.string.home_option_widget_options_description, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index b7aafa3d..173e9162 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -16,7 +16,7 @@ import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils import com.maubis.scarlet.base.export.data.* -import com.maubis.scarlet.base.settings.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.utils.log import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope @@ -583,7 +583,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { return } - val logInfo ="onRemoteRemove(${type.name}, ${data.uuid})" + val logInfo = "onRemoteRemove(${type.name}, ${data.uuid})" log("GDriveRemote", logInfo) incrementPendingSyncs(logInfo) when (type) { From 8a3227a28fd81981215e9211f61e68e278e35cd0 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 28 Jul 2019 18:39:43 +0100 Subject: [PATCH 046/134] [Strings] Adding Translations for more text and Polish --- base/src/main/res/values-ar/strings.xml | 93 +++++ base/src/main/res/values-bn/strings.xml | 93 +++++ base/src/main/res/values-de/strings.xml | 93 +++++ base/src/main/res/values-es/strings.xml | 94 +++++ base/src/main/res/values-fr/strings.xml | 93 +++++ base/src/main/res/values-hi/strings.xml | 93 +++++ base/src/main/res/values-it/strings.xml | 93 +++++ base/src/main/res/values-iw/strings.xml | 93 +++++ base/src/main/res/values-ja/strings.xml | 93 +++++ base/src/main/res/values-ko/strings.xml | 93 +++++ base/src/main/res/values-mr/strings.xml | 93 +++++ base/src/main/res/values-ms/strings.xml | 93 +++++ base/src/main/res/values-pa/strings.xml | 93 +++++ base/src/main/res/values-pl/strings.xml | 390 ++++++++++++++++++++ base/src/main/res/values-pt/strings.xml | 93 +++++ base/src/main/res/values-ru/strings.xml | 94 +++++ base/src/main/res/values-tr/strings.xml | 93 +++++ base/src/main/res/values-zh-rCN/strings.xml | 69 +++- base/src/main/res/values-zh-rHK/strings.xml | 93 +++++ base/src/main/res/values-zh-rMO/strings.xml | 93 +++++ base/src/main/res/values-zh-rTW/strings.xml | 93 +++++ base/src/main/res/values-zh/strings.xml | 93 +++++ base/src/main/res/values/strings.xml | 192 +++++----- 23 files changed, 2416 insertions(+), 97 deletions(-) create mode 100644 base/src/main/res/values-pl/strings.xml diff --git a/base/src/main/res/values-ar/strings.xml b/base/src/main/res/values-ar/strings.xml index d43f57c2..e8d51ac2 100644 --- a/base/src/main/res/values-ar/strings.xml +++ b/base/src/main/res/values-ar/strings.xml @@ -361,4 +361,97 @@ ملاحظات حديثة + + دمج الملاحظات + تصدير كما تخفيض السعر + تصدير الملاحظات في شكل تخفيض السعر. (لا يمكنك استيراد هذه العودة إلى التطبيق) + مجلد التصدير + اختر مكان تخزين جميع الصادرات + استخدام لون موضوع للخلفية + استخدام لون المذكرة للخلفية + مكن + تعطيل + تمكين مزامنة المجلد + مجلد مزامنة + مزامنة جميع الملاحظات والعلامات والمجلدات الخاصة بك إلى مجلد خارجي. يساعدك هذا في مزامنة استخدام تطبيقات أخرى بين الأجهزة ، وكذلك الحصول على نسخة في حال كنت بحاجة إلى حذف التطبيق. + تصدير إلى مجلد + مزامنة جميع الملاحظات والعلامات وغيرها إلى مجلد خارجي + تصدير الملاحظات المقفلة + مرتب حسب الحروف الأبجدية + متوفر على Scarlet Pro + النسخ الاحتياطي أيضا الملاحظات التي يتم تأمين + خيارات المحرر + تغيير الإعدادات والاستخدام لمحرر الملاحظات + خيارات تخفيض السعر الافتراضية + أظهر الأزرار السريعة للتخفيض على شريط الأدوات كإعداد افتراضي + تخفيض السعر الفعلي + اختر ما إذا كنت تريد أن يكون تخفيض السعر مرئيًا كنوعك. التمكين يمكن أن يؤثر على الأداء على الملاحظات الكبيرة. + نقل العناصر المحددة + تنتقل العناصر المحددة إلى أسفل القائمة. إلغاء تحديد لا إعادة تعيين الموقف. + إظهار مقابض الحركة + أظهر المقبض لتحريك العناصر لأعلى أو لأسفل. لا يزال بإمكانك تحريك الأشياء عن طريق لمس الزاوية. + أمثلة تخفيض السعر + إعدادات القطعة + تغيير الإعدادات لعناصر واجهة مستخدم الشاشة الرئيسية + تمكين التنسيق + يتم تقديم الملاحظات مع التنسيق في عناصر واجهة تعامل الشاشة الرئيسية + إظهار الملاحظات المقفلة + السماح بإظهار الملاحظات المقفلة في عناصر واجهة تعامل الشاشة الرئيسية + إظهار الملاحظات المؤرشفة + السماح بعرض الملاحظات المؤرشفة في عناصر واجهة تعامل الشاشة الرئيسية + إظهار الملاحظات في المهملات + السماح بإظهار الملاحظات في المهملات في عناصر واجهة تعامل الشاشة الرئيسية + مساعدة والأسئلة الشائعة + ابحث عن مساعدة حول كيفية استخدام الميزات الموجودة في التطبيق + المزامنة + في انتظار النسخ الاحتياطي + خيارات للمطور + تغيير الإعدادات الداخلية إلى التطبيق المستخدم للتصحيح + استثناءات السجل + سجل اشتعلت استثناءات إلى ملاحظة ثابتة ، للسماح لإعادة توجيه إلى المطور + إظهار ورقة الاستثناء + إظهار الاستثناءات التي تم اكتشافها على ورقة إن أمكن ، للسماح بإعادة التوجيه إلى Developer + رمي على استثناءات + رمي وتعطل التطبيق على استثناءات. سيتم إعادة تعيين بعد 5 حوادث + تمكين ملء الشاشة + اجعل التطبيق بملء الشاشة للسماح بلقطات وتسجيلات + إظهار ملاحظة UUIDs + عرض معرفات فريدة من الملاحظات في عرض الشاشة الرئيسية + استثناءات وهمية + رمي استثناء وهمية لاختبار ميزات الاستثناء + استثناء الرمية + تحطم التطبيقات + بريد + فشل الاتصال + المزامنة على Google Drive + يتيح لك Google Drive مزامنة ملاحظاتك بين الأجهزة باستخدام حساب Google Drive الخاص بك. + تسجيل الدخول إلى جوجل Firebase من قبل؟ + تسجيل الدخول إلى Google Drive + تسجيل الدخول + تسجيل الخروج من القرمزي + توقف عن مزامنة الملاحظات والعلامات والمجلدات. + توقف عن مزامنة الملاحظات والعلامات والمجلدات مع Google Drive. + ستظل بياناتك على التطبيق وعلى Google Drive موجودة. + تسجيل الخروج من Drive + تسجيل الخروج + يتم تخزين الملاحظات والعلامات والمجلدات على Google Drive ، بحيث يمكنك التحكم في الوصول إليها فقط. + يتم تحميل صورك ومزامنتها عبر الأجهزة أيضًا. + استعادة البيانات من Firebase + الدخول مع جوجل + تسجيل الدخول + اعتدنا على تخزين معلوماتك على Google Firebase. بعد تسجيل الدخول سنقوم باسترداد هذه إلى جهازك + لن تتم مزامنة الملاحظات والتغييرات الجديدة مع Google Firebase ، وستحتاج إلى تسجيل الدخول إلى Google Drive + بمجرد استرداد ملاحظاتك ، يمكنك حذفها من Google Firebase ، ثم التبديل + لا تتم مزامنة بياناتك! + تم تعطيل مزامنة البيانات استنادًا إلى تسجيل الدخول القديم ، فكّر في التبديل إلى المزامنة القائمة على Google Drive. + نقل البيانات من Firebase إلى Drive + مسح البيانات وتسجيل الخروج + المقاصة وتسجيل الخروج + اعتدنا على تخزين معلوماتك على Google Firebase. نحن ننتقل إلى تخزين ملاحظاتك في Google Drive. + مزيد من الخصوصية. + مزامنة الصور. + الخطوات التالية + سنقوم أولاً بحذف الملاحظات الخاصة بك من Firebase وتسجيل الخروج. + يمكنك بعد ذلك تسجيل الدخول إلى Google Drive ، وسنقوم بتحميل بياناتك مع الصور. + diff --git a/base/src/main/res/values-bn/strings.xml b/base/src/main/res/values-bn/strings.xml index 1920a896..8acc85b9 100644 --- a/base/src/main/res/values-bn/strings.xml +++ b/base/src/main/res/values-bn/strings.xml @@ -316,4 +316,97 @@ অ্যাপ্লিকেশন নোট, ট্যাগ এবং ফোল্ডার মুছে দিন নোট ভিউয়ার ব্যাকগ্রাউন্ড + + নোট মার্জ করুন + মার্কডাউন হিসাবে রপ্তানি করুন + মার্কডাউন বিন্যাসে নোট রপ্তানি করুন। (আপনি এই অ্যাপ্লিকেশন ফিরে এই আমদানি করতে পারবেন না) + ফোল্ডার রপ্তানি করুন + সব রপ্তানি সংরক্ষিত হয় যেখানে চয়ন করুন + পটভূমি জন্য থিম রঙ ব্যবহার করুন + পটভূমি জন্য নোট রঙ ব্যবহার করুন + সক্ষম করা + অক্ষম + ফোল্ডার সিঙ্ক সক্ষম করুন + ফোল্ডার সিঙ্ক + একটি বহিরাগত ফোল্ডারে আপনার সমস্ত নোট, ট্যাগ এবং ফোল্ডার সিঙ্ক করুন। এটি আপনাকে ডিভাইসগুলির মধ্যে অন্য অ্যাপ্লিকেশানগুলি ব্যবহার করে সিঙ্ক করতে সহায়তা করে এবং সেই অ্যাপ্লিকেশানটি মুছে ফেলার ক্ষেত্রে একটি অনুলিপি রয়েছে। + ফোল্ডারে এক্সপোর্ট করা হচ্ছে + একটি বহিরাগত ফোল্ডারে সব নোট, ট্যাগ ইত্যাদি সিঙ্ক করুন + রপ্তানি লক নোট + বর্ণানুক্রমিক + Scarlet প্রো পাওয়া যায় + এছাড়াও লক করা হয় যা নোট ব্যাকআপ + সম্পাদক অপশন + নোট সম্পাদকের জন্য সেটিংস এবং ব্যবহার পরিবর্তন করুন + ডিফল্ট হিসাবে চিহ্নিত বিকল্প + ডিফল্ট হিসাবে সরঞ্জামদণ্ডে মার্কডাউন দ্রুত বোতামগুলি দেখান + রিয়েলটাইম Markdown + আপনি আপনার টাইপ হিসাবে চিহ্নিত হতে চান চিহ্নিত করুন। সক্রিয় বড় নোট কর্মক্ষমতা প্রভাবিত করতে পারেন। + চেক করা আইটেম সরান + চেক আইটেম তালিকা নীচে সরানো। আনচেক অবস্থান পুনরায় সেট না। + আন্দোলন হ্যান্ডলগুলি দেখান + আইটেম উপরে বা নিচে সরানোর জন্য হ্যান্ডেল দেখান। আপনি এখনও কোণার স্পর্শ দ্বারা চারপাশে জিনিস সরাতে পারেন। + মার্কডাউন উদাহরণ + উইজেট সেটিংস + হোম স্ক্রিন উইজেট জন্য সেটিংস পরিবর্তন করুন + ফরম্যাটিং সক্রিয় করুন + নোট হোম স্ক্রিন উইজেটে বিন্যাস সহ রেন্ডার করা হয় + লক নোট দেখান + লক করা নোটগুলি হোম স্ক্রীন উইজেটগুলিতে দেখানোর অনুমতি দিন + সংরক্ষণাগারভুক্ত নোট প্রদর্শন করুন + আর্কাইভ নোটগুলি হোম স্ক্রীন উইজেটগুলিতে দেখানোর অনুমতি দিন + ট্র্যাশে নোট দেখান + ট্র্যাশে নোটগুলি হোম স্ক্রীন উইজেটগুলিতে দেখানোর অনুমতি দিন + সাহায্য এবং সাধারণ প্রশ্ন + অ্যাপ্লিকেশন বৈশিষ্ট্য ব্যবহার করার জন্য সাহায্য খুঁজুন + সিঙ্ক করা হচ্ছে + মুলতুবি ব্যাকআপ + বিকাশকারী বিকল্প + ডিবাগিংয়ের জন্য ব্যবহৃত অ্যাপ্লিকেশনে অভ্যন্তরীণ সেটিংস পরিবর্তন করুন + লগ ব্যতিক্রম + ডেভেলপারকে ফরোয়ার্ড করার জন্য একটি নির্দিষ্ট নোটে লগগুলি ব্যতিক্রমগুলি ধরা হয়েছে + ব্যতিক্রম পত্রক দেখান + বিকাশকারীকে ফরোয়ার্ড করার জন্য, সম্ভব হলে একটি শীটে ধরা ব্যতিক্রমগুলি দেখান + ব্যতিক্রম উপর নিক্ষেপ + থ্রেড এবং ব্যতিক্রম ব্যতিক্রম অ্যাপ্লিকেশন। 5 ক্র্যাশ পরে রিসেট করা হবে + পূর্ণস্ক্রীন সক্রিয় করুন + স্ক্রিনশট এবং রেকর্ডিং অনুমতি দিতে পূর্ণস্ক্রীন অ্যাপ্লিকেশন করুন + নোট UUID দেখান + হোম স্ক্রীন ভিউতে নোটগুলির অনন্য আইড দেখান + জাল আপত্তি + ব্যতিক্রম বৈশিষ্ট্য পরীক্ষা করার জন্য একটি জাল ব্যতিক্রম নিক্ষেপ + ব্যতিক্রম নিক্ষিপ্ত + ক্র্যাশ অ্যাপ্লিকেশন + মেল + সংযোগ ব্যর্থ হয়েছে + Google ড্রাইভে সিঙ্ক করুন + Google ড্রাইভ আপনাকে আপনার নিজের Google ড্রাইভ অ্যাকাউন্ট ব্যবহার করে ডিভাইসগুলির মধ্যে আপনার নোটগুলি সিঙ্ক করতে দেয়। + আগে গুগল ফায়ারবসে লগ ইন? + গুগল ড্রাইভে সাইন ইন করুন + সাইন ইন করুন | + স্কারলেট সাইন আউট + নোট, ট্যাগ এবং ফোল্ডার সিঙ্ক করা বন্ধ করুন। + আপনার Google ড্রাইভে আপনার নোট, ট্যাগ এবং ফোল্ডার সিঙ্ক করা বন্ধ করুন। + অ্যাপ্লিকেশন এবং Google ড্রাইভে আপনার ডেটা এখনও থাকবে। + ড্রাইভ থেকে সাইন আউট + সাইন আউট + নোট, ট্যাগ এবং ফোল্ডার আপনার নিজের Google ড্রাইভে সংরক্ষণ করা হয়, তাই শুধুমাত্র আপনি তাদের অ্যাক্সেস নিয়ন্ত্রণ করতে পারেন। + আপনার ফটো আপলোড এবং ডিভাইস জুড়ে সিঙ্ক করা হয়। + Firebase থেকে তথ্য পুনরুদ্ধার করুন + গুগলের সাথে সাইন ইন করুন + সাইন ইন করুন | + আমরা গুগল ফায়ারবসে আপনার তথ্য সংরক্ষণ করতে ব্যবহার করতাম। লগ ইন করার পরে আমরা আপনার ডিভাইসে এই পুনরুদ্ধার করা হবে + নতুন নোট এবং পরিবর্তনগুলি Google Firebase এ সিঙ্ক করা হবে না এবং আপনাকে Google ড্রাইভে লগইন করতে হবে + একবার আপনার নোট পুনরুদ্ধার হয়ে গেলে, আপনি তাদের Google ফায়ারবেস থেকে মুছতে পারেন এবং তারপরে স্যুইচ করতে পারেন + আপনার তথ্য সিঙ্ক হচ্ছে না! + লিগ্যাসি লগইন ভিত্তিক ডেটা সিঙ্ক অক্ষম করা হয়েছে, Google ড্রাইভ ভিত্তিক সিঙ্কে স্যুইচ করার কথা বিবেচনা করুন। + ফায়ারবেস থেকে ড্রাইভে ডেটা স্থানান্তর করুন + সাফ ডেটা এবং সাইন আউট + ক্লিয়ারিং এবং সাইন আউট + আমরা গুগল ফায়ারবসে আপনার তথ্য সংরক্ষণ করতে ব্যবহার করতাম। আমরা আপনার Google ড্রাইভে আপনার নোট সংরক্ষণ করতে চলে যাচ্ছি। + আরো গোপনীয়তা। + ছবি সিঙ্ক। + পরবর্তী পদক্ষেপ + আমরা প্রথমে ফায়ারबेस থেকে আপনার নোট মুছে ফেলব এবং আপনাকে লগ আউট করব। + তারপর আপনি Google ড্রাইভে লগ ইন করতে পারেন এবং আমরা আপনার ডেটা চিত্র সহ আপলোড করব। + \ No newline at end of file diff --git a/base/src/main/res/values-de/strings.xml b/base/src/main/res/values-de/strings.xml index 06e67ed3..dbfacddc 100644 --- a/base/src/main/res/values-de/strings.xml +++ b/base/src/main/res/values-de/strings.xml @@ -314,4 +314,97 @@ Löschen Sie Notizen, Tags und Ordner in der App Hinweis Viewer-Hintergrund + + Notizen zusammenführen + Als Abschrift exportieren + Notizen im Abschriftenformat exportieren. (Sie können diese nicht zurück in die App importieren.) + Ordner exportieren + Wählen Sie, wo alle Exporte gespeichert werden + Verwenden Sie die Themenfarbe für den Hintergrund + Verwenden Sie die Notenfarbe für den Hintergrund + Aktivieren + Deaktivieren + Aktivieren Sie die Ordnersynchronisierung + Ordnersynchronisation + Synchronisieren Sie alle Ihre Notizen, Tags und Ordner mit einem externen Ordner. Auf diese Weise können Sie mit anderen Apps zwischen Geräten synchronisieren und eine Kopie haben, falls Sie die App löschen müssen. + In Ordner exportieren + Synchronisieren Sie alle Notizen, Tags usw. mit einem externen Ordner + Gesperrte Notizen exportieren + Alphabetisch + Verfügbar für Scarlet Pro + Sichern Sie auch die gesperrten Notizen + Editoroptionen + Ändern Sie die Einstellungen und die Verwendung für den Notizeditor + Abschriftenoptionen als Standard + Standardmäßig werden die Abschriftenschaltflächen in der Symbolleiste angezeigt + Echtzeit Markdown + Wählen Sie aus, ob Markdown als Typ angezeigt werden soll. Das Aktivieren kann sich auf die Leistung großer Noten auswirken. + Markierte Objekte verschieben + Markierte Elemente werden an das Ende der Liste verschoben. Deaktivieren setzt die Position nicht zurück. + Bewegungsgriffe anzeigen + Zeigen Sie auf den Griff, um Elemente nach oben oder unten zu verschieben. Sie können immer noch Dinge bewegen, indem Sie die Ecke berühren. + Abschriftenbeispiele + Widget-Einstellungen + Ändern Sie die Einstellungen für die Widgets des Startbildschirms + Formatierung aktivieren + Notizen werden mit der Formatierung in Widgets des Startbildschirms gerendert + Gesperrte Notizen anzeigen + Zulassen, dass gesperrte Notizen in Widgets auf dem Startbildschirm angezeigt werden + Archivierte Notizen anzeigen + Ermöglichen, dass archivierte Notizen in Widgets auf dem Startbildschirm angezeigt werden + Notizen im Papierkorb anzeigen + Zulassen, dass Notizen im Papierkorb in Widgets auf dem Startbildschirm angezeigt werden + Hilfe und häufig gestellte Fragen + Hier finden Sie Hilfe zur Verwendung der Funktionen in der App + Synchronisierung + Ausstehende Sicherung + Entwickleroptionen + Ändern Sie die internen Einstellungen in der Anwendung, die zum Debuggen verwendet wird + Ausnahmen protokollieren + Erfasste Ausnahmen in einer festen Notiz protokollieren, um die Weiterleitung an Developer zu ermöglichen + Ausnahmeblatt anzeigen + Zeigen Sie gefangene Ausnahmen nach Möglichkeit auf einem Blatt an, um die Weiterleitung an Developer zu ermöglichen + Ausnahmen auslösen + Wirf und stürze die Anwendung in Ausnahmefällen ab. Wird nach 5 Abstürzen zurückgesetzt + Vollbild aktivieren + Erstellen Sie die App im Vollbildmodus, um Screenshots und Aufzeichnungen zuzulassen + Hinweis-UUIDs anzeigen + Zeigen Sie die eindeutigen IDs der Notizen in der Startbildschirmansicht an + Gefälschte Ausnahmen + Wirf eine gefälschte Ausnahme aus, um die Ausnahmefunktionen zu testen + Ausnahme ausgelöst + Crash App + Mail + Verbindung fehlgeschlagen + Synchronisieren Sie auf Google Drive + Mit Google Drive können Sie Ihre Notizen mit Ihrem eigenen Google Drive-Konto zwischen Geräten synchronisieren. + Bei Google Firebase angemeldet? + Melden Sie sich bei Google Drive an + Anmelden … + Melden Sie sich von Scarlet ab + Stoppen Sie die Synchronisierung von Notizen, Tags und Ordnern. + Synchronisieren Sie Ihre Notizen, Tags und Ordner nicht mehr mit Google Drive. + Ihre Daten in der App und auf Google Drive bleiben erhalten. + Vom Laufwerk abmelden + Abmelden … + Notizen, Tags und Ordner werden auf Ihrem eigenen Google Drive gespeichert, sodass nur Sie den Zugriff darauf steuern können. + Ihre Fotos werden auch geräteübergreifend hochgeladen und synchronisiert. + Daten aus Firebase wiederherstellen + Anmeldung mit Google + Anmelden … + Wir haben Ihre Daten in Google Firebase gespeichert. Nach dem Einloggen stellen wir diese auf Ihrem Gerät wieder her + Neue Notizen und Änderungen werden NICHT mit Google Firebase synchronisiert, und Sie müssen sich bei Google Drive anmelden + Sobald Ihre Notizen wiederhergestellt sind, können Sie sie aus Google Firebase löschen und dann umschalten + Ihre Daten werden nicht synchronisiert! + Die Datensynchronisierung basierend auf der alten Anmeldung ist deaktiviert. Überlegen Sie, ob Sie auf die Google Drive-basierte Synchronisierung umsteigen möchten. + Übertragen Sie Daten von Firebase auf das Laufwerk + Daten löschen und abmelden + Löschen und Abmelden + Wir haben Ihre Daten in Google Firebase gespeichert. Wir werden Ihre Notizen in Ihrem Google Drive speichern. + Mehr Privatsphäre. + Foto-Synchronisierung. + Nächste Schritte + Wir werden zuerst Ihre Notizen aus Firebase löschen und Sie abmelden. + Sie können sich dann bei Google Drive anmelden. Wir laden Ihre Daten zusammen mit Bildern hoch. + diff --git a/base/src/main/res/values-es/strings.xml b/base/src/main/res/values-es/strings.xml index 9018008e..751302a1 100644 --- a/base/src/main/res/values-es/strings.xml +++ b/base/src/main/res/values-es/strings.xml @@ -315,4 +315,98 @@ Eliminar notas, etiquetas y carpetas en la aplicación Fondo del visor de notas + + + Notas de fusión + Exportar como Markdown + Exportar notas en formato markdown. (No puede importar estos de nuevo a la aplicación) + Carpeta de exportación + Elija donde se almacenan todas las exportaciones + Utilice el color del tema para el fondo + Usa el color de la nota para el fondo. + Habilitar + Inhabilitar + Habilitar sincronización de carpetas + Sincronización de carpetas + Sincronice todas sus notas, etiquetas y carpetas en una carpeta externa. Esto le ayuda a sincronizar usando otras aplicaciones entre dispositivos, así como tener una copia en caso de que necesite eliminar la aplicación. + Exportando a la carpeta + Sincroniza todas las notas, etiquetas, etc. a una carpeta externa. + Exportar notas bloqueadas + Alfabético + Disponible en Scarlet Pro + También copia de seguridad de las notas que están bloqueadas + Opciones del editor + Cambiar la configuración y el uso del editor de notas. + Opciones de Markdown como predeterminadas + Mostrar los botones rápidos de reducción en la barra de herramientas por defecto + Markdown en tiempo real + Elija si desea que la reducción sea visible como su tipo. Habilitar puede afectar el rendimiento en grandes notas. + Mover elementos marcados + Los elementos marcados se mueven al final de la lista. Desmarcar no restablece la posición. + Mostrar manijas de movimiento + Mostrar el controlador para mover elementos hacia arriba o hacia abajo. Todavía puedes mover cosas tocando la esquina. + Ejemplos de rebajas + Configuracion de Widget + Cambiar la configuración de los widgets de la pantalla de inicio. + Habilitar formato + Las notas se procesan con formato en los widgets de la pantalla de inicio + Mostrar notas bloqueadas + Permitir que las notas bloqueadas se muestren en los widgets de la pantalla de inicio + Mostrar notas archivadas + Permitir que las notas archivadas se muestren en los widgets de la pantalla de inicio + Mostrar notas en la basura + Permitir que las notas en la papelera se muestren en los widgets de la pantalla de inicio + Ayuda y preguntas comunes + Encuentre ayuda sobre cómo usar las funciones en la aplicación + Sincronizando + Copia de seguridad pendiente + Opciones de desarrollador + Cambiar la configuración interna de la aplicación utilizada para la depuración. + Excepciones de registro + Registre las excepciones detectadas en una nota fija, para permitir el reenvío al desarrollador + Mostrar hoja de excepciones + Mostrar excepciones capturadas en una hoja si es posible, para permitir el reenvío al Desarrollador + Lanzar en excepciones + Lanzar y bloquear la aplicación en excepciones. Se reiniciará después de 5 choques. + Habilitar pantalla completa + Haga la aplicación en pantalla completa para permitir capturas de pantalla y grabaciones. + Mostrar notas UUIDs + Muestra los ID únicos de las notas en la vista de la pantalla de inicio + Falsas excepciones + Lanzar una excepción falsa para probar las características de excepción + Excepción lanzada + Aplicación Crash + Correo + La conexión falló + Sincronizar en Google Drive + Google Drive le permite sincronizar sus notas entre dispositivos usando su propia cuenta de Google Drive. + ¿Has iniciado sesión en Google Firebase antes? + Inicia sesión en Google Drive + Ingresando enâ € ¦ + Cerrar sesión de Scarlet + Deja de sincronizar notas, etiquetas y carpetas. + Deje de sincronizar sus notas, etiquetas y carpetas a su Google Drive. + Sus datos en la aplicación y en Google Drive todavía estarán allí. + Cerrar sesión de Drive + Cerrar sesión … + Las notas, etiquetas y carpetas se almacenan en su propio Google Drive, de modo que solo usted puede controlar el acceso a ellas. + Sus fotos también se cargan y sincronizan en todos los dispositivos. + Restaurar datos desde Firebase + Inicia sesión con Google + Ingresando enâ € ¦ + Solíamos almacenar su información en Google Firebase. Después de iniciar sesión los recuperaremos en su dispositivo. + Las nuevas notas y los cambios NO se sincronizarán con Google Firebase, y debe iniciar sesión en Google Drive + Una vez que se recuperan sus notas, puede eliminarlas de Google Firebase y luego cambiar + Sus datos no se están sincronizando! + La sincronización de datos basada en el inicio de sesión heredado está deshabilitada, considere cambiar a la sincronización basada en Google Drive. + Transfiere datos de Firebase a Drive + Borrar datos y cerrar sesión + Borrado y cierre de sesión + Solíamos almacenar su información en Google Firebase. Nos estamos moviendo para almacenar sus notas en su Google Drive. + Más privacidad. + Sincronización de fotos + Próximos pasos + Primero eliminaremos sus notas de Firebase y cerraremos sesión. + Luego, puede iniciar sesión en Google Drive y cargaremos sus datos junto con las imágenes. + \ No newline at end of file diff --git a/base/src/main/res/values-fr/strings.xml b/base/src/main/res/values-fr/strings.xml index 24b7e6d0..5646ec86 100644 --- a/base/src/main/res/values-fr/strings.xml +++ b/base/src/main/res/values-fr/strings.xml @@ -314,4 +314,97 @@ Supprimer des notes, des balises et des dossiers dans l\'application Note Viewer Background + + Fusionner les notes + Exporter comme démarque + Exporter des notes au format markdown. (Vous ne pouvez pas les importer dans l\'application) + Dossier d\'exportation + Choisissez où toutes les exportations sont stockées + Utiliser la couleur du thème pour le fond + Utiliser la couleur de note pour le fond + Activer + Désactiver + Activer la synchronisation des dossiers + Synchronisation des dossiers + Synchronisez toutes vos notes, balises et dossiers dans un dossier externe. Cela vous aide à synchroniser en utilisant d\'autres applications entre appareils, ainsi qu\'à en avoir une copie au cas où vous auriez besoin de supprimer l\'application. + Exporter vers un dossier + Synchronisez toutes les notes, balises, etc. dans un dossier externe. + Exporter les notes verrouillées + Alphabétique + Disponible sur Scarlet Pro + Sauvegardez également les notes qui sont verrouillées + Options de l\'éditeur + Modifier les paramètres et l\'utilisation de l\'éditeur de notes + Options de démarquage par défaut + Afficher les boutons rapides de démarquage dans la barre d’outils par défaut + Markdown en temps réel + Choisissez si vous voulez que le démarquage soit visible en tant que votre type. L\'activation peut affecter les performances sur les notes volumineuses. + Déplacer les éléments cochés + Les éléments cochés sont déplacés au bas de la liste. Décocher ne réinitialise pas la position. + Afficher les poignées de mouvement + Afficher la poignée pour déplacer les éléments vers le haut ou le bas. Vous pouvez toujours déplacer des objets en touchant le coin. + Exemples de démarques + Paramètres du widget + Modifier les paramètres des widgets de l\'écran d\'accueil + Activer le formatage + Les notes sont rendues avec le formatage dans les widgets de l\'écran d\'accueil. + Afficher les notes verrouillées + Autoriser l\'affichage des notes verrouillées dans les widgets de l\'écran d\'accueil + Afficher les notes archivées + Autoriser l\'affichage des notes archivées dans les widgets de l\'écran d\'accueil + Afficher les notes dans la corbeille + Autoriser l\'affichage des notes dans la corbeille dans les widgets de l\'écran d\'accueil + Aide et questions communes + Trouver de l\'aide sur l\'utilisation des fonctionnalités de l\'application + Synchronisation + Sauvegarde en attente + Options de développeur + Modifier les paramètres internes de l\'application utilisée pour le débogage + Enregistrer les exceptions + Journalise les exceptions interceptées dans une note fixe, pour permettre le transfert vers le développeur + Afficher la feuille d\'exception + Afficher les exceptions interceptées sur une feuille, si possible, pour permettre le transfert au développeur + Lancer sur les exceptions + Lancer et planter l\'application sur les exceptions. Sera réinitialisé après 5 accidents + Activer le plein écran + Créez l\'application en plein écran pour autoriser les captures d\'écran et les enregistrements + Afficher les UUID de note + Afficher les identifiants uniques des notes dans la vue de l\'écran d\'accueil + Fausses exceptions + Lancer une fausse exception pour tester les fonctionnalités de l\'exception + Exception levée + Crash App + Courrier + La connexion a échoué + Synchroniser sur Google Drive + Google Drive vous permet de synchroniser vos notes entre appareils en utilisant votre propre compte Google Drive. + Connecté à Google Firebase avant? + Connectez-vous à Google Drive. + Ouverture de session… + Déconnexion de Scarlet + Arrêtez la synchronisation des notes, des balises et des dossiers. + Arrêtez de synchroniser vos notes, tags et dossiers sur votre Google Drive. + Vos données sur l\'application et sur Google Drive seront toujours là. + Déconnexion de Drive + Se déconnecter… + Les notes, les balises et les dossiers sont stockés sur votre propre Google Drive. Vous êtes donc le seul à pouvoir en contrôler l\'accès. + Vos photos sont également téléchargées et synchronisées sur plusieurs appareils. + Restaurer les données de Firebase + Connectez-vous avec Google + Ouverture de session… + Nous avions l\'habitude de stocker vos informations sur Google Firebase. Après la connexion, nous les récupérerons sur votre appareil. + Les nouvelles notes et modifications NE seront PAS synchronisées sur Google Firebase et vous devez vous connecter à Google Drive. + Une fois vos notes récupérées, vous pouvez les supprimer de Google Firebase, puis basculer + Vos données ne sont pas synchronisées! + La synchronisation des données basée sur la connexion héritée est désactivée, envisagez de passer à la synchronisation basée sur Google Drive. + Transférer des données de Firebase à Drive + Effacer les données et se déconnecter + Dégagement et déconnexion + Nous avions l\'habitude de stocker vos informations sur Google Firebase. Nous allons maintenant stocker vos notes dans votre Google Drive. + Plus d\'intimité. + Sync Photo + Prochaines étapes + Nous allons d\'abord supprimer vos notes de Firebase et vous déconnecter. + Vous pouvez ensuite vous connecter à Google Drive et nous téléchargerons vos données avec les images. + diff --git a/base/src/main/res/values-hi/strings.xml b/base/src/main/res/values-hi/strings.xml index 3e0fefaa..5904d7c2 100644 --- a/base/src/main/res/values-hi/strings.xml +++ b/base/src/main/res/values-hi/strings.xml @@ -314,4 +314,97 @@ ऐप में नोट्स, टैग और फ़ोल्डर्स हटाएं नोट दर्शक पृष्ठभूमि + + नोट्स मिलाएं + मार्कडाउन के रूप में निर्यात करें + मार्कडाउन प्रारूप में नोट्स निर्यात करें। (आप इन एप्लिकेशन को वापस आयात नहीं कर सकते) + निर्यात फ़ोल्डर + चुनें कि सभी निर्यात कहाँ संग्रहीत हैं + पृष्ठभूमि के लिए थीम रंग का उपयोग करें + पृष्ठभूमि के लिए नोट रंग का उपयोग करें + सक्षम करें + अक्षम + फ़ोल्डर सिंक सक्षम करें + फ़ोल्डर सिंक + अपने सभी नोट्स, टैग और फ़ोल्डर्स को बाहरी फ़ोल्डर में सिंक करें। यह आपको डिवाइस के बीच अन्य एप्लिकेशन का उपयोग करके सिंक करने में मदद करता है, साथ ही आपको एप्लिकेशन को हटाने की आवश्यकता होने की स्थिति में एक कॉपी भी है। + फ़ोल्डर में निर्यात कर रहा है + सभी नोट्स, टैग आदि को किसी बाहरी फ़ोल्डर में सिंक करें + बंद नोट निर्यात करें + वर्णमाला + स्कारलेट प्रो पर उपलब्ध है + जो नोट बंद हैं उनका भी बैकअप लें + संपादक विकल्प + नोट संपादक के लिए सेटिंग्स और उपयोग बदलें + डिफ़ॉल्ट के रूप में मार्कडाउन विकल्प + डिफ़ॉल्ट रूप में टूलबार पर मार्कडाउन क्विक बटन दिखाएं + रियलटाइम मार्कडाउन + चुनें कि क्या आप मार्कडाउन को अपने प्रकार के रूप में देखना चाहते हैं। सक्षम करना बड़े नोटों पर प्रदर्शन को प्रभावित कर सकता है। + चेक किए गए आइटम ले जाएं + चेक किए गए आइटम सूची के निचले भाग में जाते हैं। अनचेक स्थिति को रीसेट नहीं करता है। + मूवमेंट हैंडल दिखाएं + आइटम को ऊपर या नीचे ले जाने के लिए हैंडल दिखाएं। आप अभी भी कोने को छूकर चीजों को घुमा सकते हैं। + मार्कडाउन उदाहरण + विजेट सेटिंग्स + होम स्क्रीन विजेट के लिए सेटिंग्स बदलें + स्वरूपण सक्षम करें + नोट्स होम स्क्रीन विजेट में प्रारूपण के साथ प्रदान किए जाते हैं + बंद नोट दिखाएं + बंद नोटों को होम स्क्रीन विजेट में दिखाने की अनुमति दें + संग्रहीत नोट दिखाएं + संग्रहीत नोटों को होम स्क्रीन विजेट में दिखाने की अनुमति दें + ट्रैश में नोट दिखाएं + होम स्क्रीन विजेट में दिखाए जाने वाले ट्रैश में नोट्स दें + सहायता और सामान्य प्रश्न + एप्लिकेशन में सुविधाओं का उपयोग करने के बारे में मदद प्राप्त करें + सिंक कर रहा है + लंबित बैकअप + डेवलपर विकल्प + डिबगिंग के लिए उपयोग किए जाने वाले एप्लिकेशन में आंतरिक सेटिंग्स बदलें + अपवाद लॉग करें + डेवलपर को अग्रेषित करने की अनुमति देने के लिए, एक निश्चित नोट के अपवादों को पकड़ लें + अपवाद शीट दिखाएं + डेवलपर को अग्रेषित करने की अनुमति देने के लिए यदि संभव हो तो एक शीट पर अपवादों को दिखाएं + अपवाद पर फेंक दो + अपवादों पर एप्लिकेशन को फेंकें और क्रैश करें। 5 क्रैश के बाद रीसेट हो जाएगा + पूर्णस्क्रीन सक्षम करें + स्क्रीनशॉट और रिकॉर्डिंग की अनुमति देने के लिए फुलस्क्रीन में ऐप बनाएं + नोट UUIDs दिखाएं + होम स्क्रीन दृश्य में नोटों की अद्वितीय आईडी दिखाएं + नकली अपवाद + अपवाद सुविधाओं का परीक्षण करने के लिए एक नकली अपवाद फेंक दें + अपवाद फेंकना + क्रैश ऐप + मेल + कनेक्शन विफल + Google डिस्क पर सिंक करें + Google ड्राइव आपको अपने स्वयं के Google ड्राइव खाते का उपयोग करके उपकरणों के बीच अपने नोट्स को सिंक करने की अनुमति देता है। + पहले Google Firebase में लॉग इन किया है? + Google ड्राइव में साइन इन करें + साइन इन करें € ¦ ¦ + स्कारलेट से साइन आउट करें + नोट्स, टैग और फ़ोल्डरों को सिंक करना बंद करें। + अपने नोट्स, टैग और फ़ोल्डरों को अपने Google ड्राइव में सिंक करना बंद करें। + ऐप और Google ड्राइव पर आपका डेटा अभी भी रहेगा। + ड्राइव से साइन आउट करें + साइन आउट करना € ¦ + नोट्स, टैग और फ़ोल्डर आपके स्वयं के Google ड्राइव पर संग्रहीत किए जाते हैं, इसलिए केवल आप उन तक पहुंच को नियंत्रित कर सकते हैं। + आपके फ़ोटो अपलोड किए गए हैं और साथ ही उपकरणों में भी सिंक किए गए हैं। + Firebase से डेटा को पुनर्स्थापित करें + Google के साथ साइन इन करें + साइन इन करें € ¦ ¦ + हम आपकी जानकारी Google Firebase पर संग्रहीत करते थे। लॉग इन करने के बाद हम इन्हें आपके डिवाइस पर रिकवर कर लेंगे + नए नोट और परिवर्तन Google Firebase पर सिंक नहीं होंगे, और आपको Google ड्राइव पर लॉगिन करना होगा + एक बार जब आपके नोट बरामद हो जाते हैं, तो आप उन्हें Google Firebase से हटा सकते हैं और फिर स्विच कर सकते हैं + आपका डेटा सिंक नहीं किया जा रहा है! + विरासत लॉगिन पर आधारित डेटा सिंक अक्षम है, Google ड्राइव आधारित सिंक पर स्विच करने पर विचार करें। + फायरबेस से ड्राइव में डेटा ट्रांसफर करें + डेटा साफ़ करें और साइन आउट करें + समाशोधन और हस्ताक्षर करना + हम आपकी जानकारी Google Firebase पर संग्रहीत करते थे। हम आपके नोट्स को आपके Google ड्राइव में संग्रहीत करने के लिए आगे बढ़ रहे हैं। + अधिक गोपनीयता। + फोटो सिंक। + अगला कदम + हम पहले आपके नोट्स को Firebase से हटाकर आपको लॉग आउट करेंगे। + फिर आप Google ड्राइव में लॉग इन कर सकते हैं, और हम आपके डेटा को छवियों के साथ अपलोड करेंगे। + \ No newline at end of file diff --git a/base/src/main/res/values-it/strings.xml b/base/src/main/res/values-it/strings.xml index 84518527..2a98a10d 100644 --- a/base/src/main/res/values-it/strings.xml +++ b/base/src/main/res/values-it/strings.xml @@ -314,4 +314,97 @@ Elimina note, tag e cartelle nell\'app Note sullo sfondo del visualizzatore + + Unisci note + Esporta come markdown + Esporta le note in formato markdown. (Non è possibile importarli nuovamente nell\'app) + Esporta cartella + Scegli dove archiviare tutte le esportazioni + Usa il colore del tema per lo sfondo + Usa il colore della nota per lo sfondo + Abilitare + disattivare + Abilita sincronizzazione cartella + Sincronizzazione cartelle + Sincronizza tutte le note, i tag e le cartelle in una cartella esterna. Questo ti aiuta a sincronizzare utilizzando altre app tra dispositivi, oltre a averne una copia nel caso in cui sia necessario eliminare l\'app. + Esportazione in cartella + Sincronizza tutte le note, i tag, ecc. In una cartella esterna + Esporta note bloccate + Alfabetico + Disponibile su Scarlet Pro + Effettua anche il backup delle note che sono bloccate + Opzioni dell\'editor + Modifica le impostazioni e l\'utilizzo per l\'editor delle note + Opzioni di markdown come predefinite + Mostra i pulsanti rapidi di markdown sulla barra degli strumenti come impostazione predefinita + Markdown in tempo reale + Scegli se vuoi che il markdown sia visibile come tipo. L\'abilitazione può influire sulle prestazioni su note di grandi dimensioni. + Sposta elementi controllati + Gli elementi selezionati si spostano in fondo all\'elenco. Deseleziona non ripristina la posizione. + Mostra maniglie di movimento + Mostra la maniglia per spostare gli elementi verso l\'alto o verso il basso. Puoi comunque spostare le cose toccando l\'angolo. + Esempi di markdown + Impostazioni del widget + Modifica le impostazioni per i widget della schermata principale + Abilita formattazione + Le note vengono visualizzate con formattazione nei widget della schermata principale + Mostra note bloccate + Consenti alle note bloccate di essere visualizzate nei widget della schermata principale + Mostra note archiviate + Consenti alle note archiviate di essere visualizzate nei widget della schermata principale + Mostra note nel cestino + Consenti alle note nel cestino di essere visualizzate nei widget della schermata principale + Aiuto e domande comuni + Trova assistenza su come utilizzare le funzionalità nell\'app + Sincronizzazione + Backup in sospeso + Opzioni sviluppatore + Modifica le impostazioni interne nell\'applicazione utilizzata per il debug + Eccezioni registro + Registra le eccezioni rilevate su una nota fissa per consentire l\'inoltro allo sviluppatore + Mostra foglio eccezioni + Mostra le eccezioni rilevate su un foglio, se possibile, per consentire l\'inoltro allo sviluppatore + Aggiungi eccezioni + Lancia e blocca l\'applicazione in caso di eccezioni. Verrà ripristinato dopo 5 arresti anomali + Abilita schermo intero + Rendi l\'app a schermo intero per consentire schermate e registrazioni + Mostra UUID note + Mostra gli ID univoci delle note nella vista della schermata principale + Eccezioni false + Lancia un\'eccezione falsa per testare le funzionalità dell\'eccezione + Eccezione generata + App Crash + posta + Connessione fallita + Sincronizza su Google Drive + Google Drive ti consente di sincronizzare le tue note tra i dispositivi utilizzando il tuo account Google Drive. + Hai effettuato l\'accesso a Google Firebase prima? + Accedi a Google Drive + Accessoâ € ¦ + Esci da Scarlet + Smetti di sincronizzare note, tag e cartelle. + Interrompi la sincronizzazione di note, tag e cartelle su Google Drive. + I tuoi dati sull\'app e su Google Drive saranno ancora lì. + Esci da Drive + Disconnessioneâ € ¦ + Note, tag e cartelle sono memorizzati sul tuo Google Drive, quindi solo tu puoi controllarne l\'accesso. + Le tue foto vengono caricate e sincronizzate anche su tutti i dispositivi. + Ripristina dati da Firebase + Accedi con Google + Accessoâ € ¦ + Conservavamo le tue informazioni su Google Firebase. Dopo aver effettuato l\'accesso, li ripristineremo sul tuo dispositivo + Le nuove note e modifiche NON verranno sincronizzate con Google Firebase e dovrai accedere a Google Drive + Una volta recuperate le note, è possibile eliminarle da Google Firebase e passare successivamente + I tuoi dati non vengono sincronizzati! + La sincronizzazione dei dati basata sull\'accesso legacy è disabilitata, considera la possibilità di passare alla sincronizzazione basata su Google Drive. + Trasferisci dati da Firebase a Drive + Cancella dati ed esci + Cancellazione e disconnessione + Conservavamo le tue informazioni su Google Firebase. Stiamo passando alla memorizzazione delle note nel tuo Google Drive. + Più privacy. + Sincronizzazione foto. + Prossimi passi + Prima elimineremo le tue note da Firebase e ti disconnetteremo. + Puoi quindi accedere a Google Drive e noi cariceremo i tuoi dati insieme alle immagini. + diff --git a/base/src/main/res/values-iw/strings.xml b/base/src/main/res/values-iw/strings.xml index b29ecf1f..346d330a 100644 --- a/base/src/main/res/values-iw/strings.xml +++ b/base/src/main/res/values-iw/strings.xml @@ -315,4 +315,97 @@ מחק הערות, תגים ותיקיות באפליקציה הערה מציג רקע + + מיזוג הערות + ייצא כסימון + ייצא הערות בפורמט סימון. (אינך יכול לייבא אותם בחזרה לאפליקציה) + ייצא תיקיה + בחר היכן יאוחסן כל היצוא + השתמש בצבע הנושא לרקע + השתמש בצבע הערה לרקע + אפשר + השבת + הפעל סנכרון תיקיות + סינכרון תיקיות + סנכרן את כל ההערות, התגים והתיקיות שלך לתיקיה חיצונית. זה עוזר לך לסנכרן שימוש באפליקציות אחרות בין מכשירים, וכן לקבל עותק למקרה שתצטרך למחוק את האפליקציה. + מייצא לתיקיה + סנכרן את כל ההערות, התגים וכו \'לתיקיה חיצונית + ייצא פתקים נעולים + אלפביתי + זמין ב- Scarlet Pro + גבה גם את הפתקים הנעולים + אפשרויות עורך + שנה את ההגדרות והשימוש עבור עורך ההערות + אפשרויות סימון כברירת מחדל + הצג את לחצני המהירות לסימון בסרגל הכלים כברירת מחדל + Markdown בזמן אמת + בחר אם ברצונך שהסימון יהיה גלוי כסוג. הפעלה יכולה להשפיע על הביצועים על תווים גדולים. + העבר פריטים מסומנים + פריטים מסומנים עוברים לתחתית הרשימה. ביטול הסימון אינו מאפס את המיקום. + הצג ידיות תנועה + הצג את הידית כדי להזיז פריטים למעלה או למטה. אתה עדיין יכול להזיז דברים על ידי נגיעה בפינה. + דוגמאות לסריקה + הגדרות יישומון + שנה את ההגדרות עבור יישומוני מסך הבית + אפשר עיצוב + הערות ניתנות באמצעות עיצוב בווידג\'טים של מסך הבית + הצג פתקים נעולים + אפשר להציג הערות נעולות בווידג\'טים של מסך הבית + הצג הערות בארכיון + אפשר להצגת הערות בארכיון בווידג\'טים של מסך הבית + הצג הערות באשפה + אפשר להצגת ההערות באשפה בווידג\'טים של מסך הבית + עזרה ושאלות נפוצות + מצא עזרה כיצד להשתמש בתכונות באפליקציה + מסנכרן + ממתין לגיבוי + אפשרויות למפתחים + שנה הגדרות פנימיות ליישום המשמש לניפוי באגים + חריגים ביומן + יומן חריגים שנתפסו לפתק קבוע, כדי לאפשר העברה למפתח + הצג גיליון חריג + הצג חריגים שנתפסו על גיליון במידת האפשר, כדי לאפשר העברה למפתח + זרוק על חריגים + זרוק את האפליקציה והתרסק אותה על חריגים. יתאפס לאחר 5 קריסות + אפשר מסך מלא + הפוך את האפליקציה למסך מלא כדי לאפשר צילומי מסך והקלטות + הצג UUIDs הערה + הצג את המזהים הייחודיים של ההערות בתצוגת מסך הבית + חריגים מזויפים + זרוק חריג מזויף לבדיקת תכונות החריגה + חריגה נזרקת + אפליקציית התרסקות + דואר + חיבור נכשל + סנכרן בכונן Google + כונן Google מאפשר לך לסנכרן את ההערות שלך בין מכשירים באמצעות חשבון Google Drive שלך. + התחברת ל- Firebase של גוגל לפני? + היכנס ל- Google Drive + חתימה על פנימה + יציאה מסקרלט + הפסק לסנכרן הערות, תגיות ותיקיות. + הפסק לסנכרן את ההערות, התגיות והתיקיות שלך לכונן Google שלך. + הנתונים שלך באפליקציה וב- Google Drive עדיין יהיו שם. + צא מכונן + יציאה החוצה … + הערות, תגים ותיקיות מאוחסנים בכונן Google שלך, כך שרק אתה יכול לשלוט על הגישה אליהם. + גם התמונות שלך מועלות ומסונכרנות בין מכשירים. + שחזר נתונים מבסיס האש + היכנס באמצעות Google + חתימה על פנימה + נהגנו לאחסן את המידע שלך ב- Google Firebase. לאחר הכניסה נשחזר אותם למכשיר שלך + הערות ושינויים חדשים לא יסונכרנו עם Google Firebase, ועליך להיכנס ל- Google Drive + לאחר שההערות שלך יתאוששו, אתה יכול למחוק אותן מ- Google Firebase ואז לעבור + הנתונים שלך לא מסונכרנים! + סנכרון נתונים המבוסס על כניסה מדור קודם אינו זמין, שקול לעבור לסנכרון מבוסס Google Drive. + העבר נתונים מכונן האש לכונן + נקה נתונים ויצא + ניקוי ויציאה + נהגנו לאחסן את המידע שלך ב- Google Firebase. אנו עוברים לאחסון הערות בכונן Google שלך. + פרטיות רבה יותר. + סינכרון תמונות. + הצעדים הבאים + אנו נמחק תחילה את ההערות שלך מ- Firebase ונצא אותך. + לאחר מכן תוכל להיכנס ל- Google Drive ונעלה את הנתונים שלך יחד עם תמונות. + \ No newline at end of file diff --git a/base/src/main/res/values-ja/strings.xml b/base/src/main/res/values-ja/strings.xml index 7c782580..908b0ded 100644 --- a/base/src/main/res/values-ja/strings.xml +++ b/base/src/main/res/values-ja/strings.xml @@ -294,4 +294,97 @@ アプリ内のメモ、タグ、フォルダを削除する ビューアの背景 + + メモをマージ + 値下げとしてエクスポート + ノートをマークダウン形式でエクスポートします。 (これらをアプリにインポートすることはできません) + エクスポートフォルダ + すべての輸出が保管されている場所を選択してください + 背景色にテーマカラーを使用 + 背景にメモの色を使用 + 有効にする + 無効にする + フォルダ同期を有効にする + フォルダ同期 + すべてのメモ、タグ、およびフォルダを外部フォルダに同期します。これにより、デバイス間で他のアプリを使用して同期したり、アプリを削除する必要がある場合に備えてコピーを作成したりできます。 + フォルダにエクスポート + すべてのメモ、タグなどを外部フォルダに同期する + ロックされたメモをエクスポートする + アルファベット順 + Scarlet Proで利用可能 + ロックされているメモもバックアップする + エディタオプション + ノートエディタの設定と使い方を変更する + デフォルトとしてのマークダウンオプション + デフォルトでマークダウンクイックボタンをツールバーに表示する + リアルタイムマークダウン + マークダウンを自分のタイプとして表示するかどうかを選択してください。有効にすると、大きな音符のパフォーマンスに影響が出る可能性があります。 + チェックしたアイテムを移動する + チェックされた項目はリストの一番下に移動します。チェックを外すと位置はリセットされません。 + 移動ハンドルを表示 + アイテムを上下に移動するためのハンドルを表示します。あなたはまだコーナーに触れることによって物事を動かすことができます。 + マークダウンの例 + ウィジェット設定 + ホーム画面ウィジェットの設定を変更する + フォーマットを有効にする + ノートはホームスクリーンウィジェットのフォーマットでレンダリングされる + ロックされたメモを表示 + ロックされたメモをホーム画面のウィジェットに表示することを許可する + アーカイブノートを表示 + アーカイブされたメモをホーム画面のウィジェットに表示することを許可する + ゴミ箱にメモを表示 + ゴミ箱のメモをホーム画面のウィジェットに表示することを許可する + ヘルプとよくある質問 + アプリの機能を使用する方法についてのヘルプを探す + 同期中 + バックアップ保留 + 開発者向けオプション + デバッグに使用するアプリケーションに内部設定を変更する + 例外ログ + 検出された例外を修正されたメモに記録して、開発者に転送できるようにします。 + 例外シートを表示 + 可能であれば、キャッチされた例外をシートに表示して、Developerに転送できるようにする + 例外を投げる + 例外が発生した場合はアプリケーションをスローしてクラッシュさせます。 5回のクラッシュ後にリセットされます + フルスクリーンを有効にする + スクリーンショットと録画を許可するようにフルスクリーンでアプリを作る + メモUUIDを表示 + ホームスクリーンビューでメモの一意のIDを表示する + 偽の例外 + 例外機能をテストするために偽の例外を投げる + スローされた例外 + クラッシュアプ​​リ + 郵便物 + 接続に失敗しました + Googleドライブで同期 + Googleドライブでは、自分のGoogleドライブアカウントを使用してデバイス間でメモを同期できます。 + 以前にGoogle Firebaseにログインしましたか? + Googleドライブにサインインする + サインイン + スカーレットからサインアウト + メモ、タグ、フォルダの同期を停止します。 + メモ、タグ、フォルダをGoogleドライブに同期しないでください。 + アプリとGoogleドライブのデータはそのまま残ります。 + ドライブからサインアウトする + ログアウトする + メモ、タグ、フォルダは自分のGoogleドライブに保存されているため、それらへのアクセスを制御できるのはあなただけです。 + あなたの写真はアップロードされ、デバイス間でも同期されます。 + Firebaseからデータを復元する + Googleでサインイン + サインイン + Google Firebaseにあなたの情報を保存していました。ログインした後、私たちはあなたのデバイスにこれらを回復します + 新しいメモや変更はGoogle Firebaseに同期されません。Googleドライブにログインする必要があります。 + メモが復元されたら、Google Firebaseからそれらを削除してから切り替えることができます。 + あなたのデータは同期されていません! + 従来のログインに基づくデータ同期は無効になっています。Googleドライブベースの同期への切り替えを検討してください。 + Firebaseからドライブへのデータ転送 + データを消去してサインアウトする + クリアとサインアウト + Google Firebaseにあなたの情報を保存していました。 Googleドライブへのメモの保存に移行しています。 + より多くのプライバシー。 + 写真の同期 + 次のステップ + まずFirebaseからあなたのメモを削除してログアウトします。 + Googleドライブにログインすると、データと共に画像がアップロードされます。 + \ No newline at end of file diff --git a/base/src/main/res/values-ko/strings.xml b/base/src/main/res/values-ko/strings.xml index ddcd89b3..09b64f42 100644 --- a/base/src/main/res/values-ko/strings.xml +++ b/base/src/main/res/values-ko/strings.xml @@ -294,4 +294,97 @@ 앱의 메모, 태그 및 폴더 삭제 뷰어 배경 참고 + + 메모 병합 + 마크 다운으로 내보내기 + 메모를 축소 형으로 내 보냅니다. (다시 앱으로 가져올 수는 없습니다.) + 폴더 내보내기 + 모든 내보내기가 저장되는 위치 선택 + 배경에 테마 색상 사용 + 배경에 노트 색상 사용 + 사용 + 사용 안함 + 폴더 동기화 사용 + 폴더 동기화 + 모든 노트, 태그 및 폴더를 외부 폴더와 동기화하십시오. 이렇게하면 기기간에 다른 앱을 사용하여 동기화하는 데 도움이되며 앱을 삭제해야하는 경우를 대비하여 사본을 얻을 수 있습니다. + 폴더로 내보내기 + 모든 노트, 태그 등을 외부 폴더와 동기화하십시오. + 잠긴 메모 내보내기 + 알파벳순 + 스칼렛 프로에서 사용 가능 + 또한 잠긴 메모를 백업하십시오. + 편집기 옵션 + 노트 편집기의 설정 및 사용법 변경 + 기본값으로 표시 옵션 + 툴바에 마크 다운 단축 버튼 표시 (기본값) + 실시간 마카오 + 유형으로 표시 할 수있는 가격 표시를 선택하십시오. 큰 노트에 대해 효과를 적용 할 수 있습니다. + 선택한 항목 이동 + 선택된 항목은 목록의 맨 아래로 이동합니다. 선택을 취소해도 위치가 재설정되지 않습니다. + 이동 핸들 표시 + 항목을 위 또는 아래로 움직이려면 핸들을 표시하십시오. 모서리를 만져도 주변을 움직일 수 있습니다. + 마크 다운 예 + 위젯 설정 + 홈 화면 위젯의 설정 변경 + 서식 사용 + 메모는 홈 스크린 위젯에서 포맷으로 렌더링됩니다. + 잠긴 메모 표시 + 잠긴 메모를 홈 화면 위젯에 표시하도록 허용 + 보관 된 메모 표시 + 보관 된 메모를 홈 화면 위젯에 표시하도록 허용 + 메모를 휴지통에 표시 + 휴지통에있는 메모를 홈 화면 위젯에 표시하도록 허용 + 도움말 및 일반적인 질문 + 앱에서 기능을 사용하는 방법에 대한 도움말을 찾아보십시오. + 동기화 중 + 보류중인 백업 + 개발자 옵션 + 디버깅에 사용되는 응용 프로그램의 내부 설정 변경 + 예외 로그 + 고정 메모에 catch 된 예외를 기록하여 개발자에게 전달할 수 있도록합니다. + 예외 시트 표시 + 가능한 경우 시트에 예외를 표시하여 개발자에게 전달할 수 있도록합니다. + 예외에 던지기 + 응용 프로그램을 예외에 대해 던집니다. 5 번의 충돌 후 재설정됩니다. + 전체 화면 사용 + 전체 화면으로 앱을 만들어 스크린 샷과 녹음을 허용합니다. + 메모 UUID 표시 + 홈 화면보기에서 노트의 고유 ID 표시 + 가짜 예외 + 가짜 예외를 던져 예외 기능을 테스트하십시오. + Throw 된 Exception + 앱 다운 + 우편 + 연결에 실패 + Google 드라이브에서 동기화 + Google 드라이브를 사용하면 자신의 Google 드라이브 계정을 사용하여 기기간에 메모를 동기화 할 수 있습니다. + 이전에 Google Firebase에 로그인하셨습니까? + Google 드라이브에 로그인 + 서명하는 중 | + 스칼렛 사인 + 메모, 태그 및 폴더 동기화를 중지합니다. + 메모, 태그 및 폴더를 Google 드라이브와 동기화하지 않습니다. + 앱 및 Google 드라이브의 데이터는 그대로 유지됩니다. + 드라이브에서 로그 아웃 + 서명 아웃 | + 메모, 태그 및 폴더는 나만의 Google 드라이브에 저장되므로 액세스 권한 만 제어 할 수 있습니다. + 사진은 기기간에 업로드되고 동기화됩니다. + Firebase에서 데이터 복원 + Google로 로그인 + 서명하는 중 | + Google은 귀하의 정보를 Google Firebase에 저장하곤했습니다. 로그인 한 후 장치로 복구합니다. + 새로운 메모와 변경 사항은 Google Firebase에 동기화되지 않으며 Google 드라이브에 로그인해야합니다. + 메모가 복구되면 Google Firebase에서 메모를 삭제 한 다음 전환 할 수 있습니다. + 데이터가 동기화되지 않습니다. + 기존 로그인을 기반으로 한 데이터 동기화가 사용 중지되었습니다. Google 드라이브 기반 동기화로 전환하는 것이 좋습니다. + Firebase에서 드라이브로 데이터 전송 + 데이터 지우기 및 로그 아웃 + 삭제 및 로그 아웃 + Google은 귀하의 정보를 Google Firebase에 저장하곤했습니다. Google은 메모를 Google 드라이브에 저장하는 작업으로 이전하고 있습니다. + 더 많은 개인 정보. + 사진 동기화. + 다음 단계 + 먼저 Firebase에서 노트를 삭제하고 로그 아웃합니다. + 그런 다음 Google 드라이브에 로그인하면 이미지와 함께 데이터가 업로드됩니다. + \ No newline at end of file diff --git a/base/src/main/res/values-mr/strings.xml b/base/src/main/res/values-mr/strings.xml index 7d59b4a0..ce7f00bb 100644 --- a/base/src/main/res/values-mr/strings.xml +++ b/base/src/main/res/values-mr/strings.xml @@ -313,4 +313,97 @@ अॅप मधील नोट्स, टॅग आणि फोल्डर हटवा नोट दर्शक पहा + + नोट्स विलीन करा + मार्कडाउन म्हणून निर्यात करा + मार्कडाउन स्वरूपनात नोट्स निर्यात करा. (आपण हे अॅपवर परत आयात करू शकत नाही) + फोल्डर निर्यात करा + सर्व निर्यात कोठे संग्रहित आहेत ते निवडा + पार्श्वभूमीसाठी थीम रंग वापरा + पार्श्वभूमीसाठी नोट रंग वापरा + सक्षम करा + अक्षम करा + फोल्डर सिंक सक्षम करा + फोल्डर समक्रमण + आपल्या सर्व नोट्स, टॅग आणि फोल्डर्स एका बाह्य फोल्डरमध्ये समक्रमित करा. हे आपल्याला डिव्हाइसेस दरम्यान इतर अॅप्स वापरुन समक्रमित करण्यात मदत करते आणि आपल्याला अॅप हटविण्याची आवश्यकता असल्यास कॉपी देखील आहे. + फोल्डरमध्ये निर्यात करीत आहे + सर्व नोट्स, टॅग्ज इ. बाह्य फोल्डरमध्ये समक्रमित करा + बंद लॉक नोट्स + वर्णानुक्रम + स्कार्लेट प्रो वर उपलब्ध + लॉक केलेल्या नोट्सचा बॅक अप देखील घ्या + संपादक पर्याय + नोट संपादकासाठी सेटिंग्ज आणि वापर बदला + डीफॉल्ट म्हणून मार्कडाउन पर्याय + टूलबारवरील मार्कडाउन त्वरित बटणे डीफॉल्ट म्हणून दर्शवा + रिअलटाइम मार्कडाउन + आपल्या प्रकारानुसार मार्कडाउन दृश्यमान होऊ इच्छित असल्यास निवडा. सक्षम करणे मोठ्या नोट्सवरील कार्यप्रदर्शन प्रभावित करू शकते. + चेक केलेले आयटम हलवा + चेक केलेले आयटम सूचीच्या तळाशी फिरतात. अनचेक स्थिती रीसेट करत नाही. + हालचाल हाताळताना दर्शवा + आयटम वर किंवा खाली हलविण्यासाठी हँडल दर्शवा. कोपऱ्याला स्पर्श करून आपण अद्याप गोष्टी हलवू शकता. + मार्कडाउन उदाहरणे + विजेट सेटिंग्ज + होम स्क्रीन विजेटसाठी सेटिंग्ज बदला + स्वरूपन सक्षम करा + होम स्क्रीन विजेटमध्ये स्वरूपनासह नोट्स प्रस्तुत केली जातात + लॉक केलेले नोट्स दर्शवा + होम स्क्रीन विजेटमध्ये लॉक केलेल्या नोट्स दर्शविण्याची परवानगी द्या + संग्रहित नोट्स दर्शवा + संग्रहित टिपा होम स्क्रीन विजेटमध्ये दर्शविण्याची परवानगी द्या + कचर्यातील नोट्स दर्शवा + होम स्क्रीन विजेटमध्ये कचर्यातील नोट्स दर्शविण्याची परवानगी द्या + मदत आणि सामान्य प्रश्न + अॅपमधील वैशिष्ट्यांचा वापर कसा करावा याबद्दल मदत शोधा + संकालन + प्रलंबित बॅकअप + विकसक पर्याय + डीबगिंगसाठी वापरल्या जाणार्या अनुप्रयोगास अंतर्गत सेटिंग्ज बदला + लॉग अपवाद + विकसकांना अग्रेषित करण्याची अनुमती देण्यासाठी लॉगने एका निश्चित टीपमध्ये अपवाद पकडले + अपवाद पत्रक दर्शवा + विकसकांना अग्रेषित करण्याची परवानगी देण्यासाठी शक्य असल्यास शीटवर पकडलेले अपवाद दर्शवा + अपवादांवर थ्रो + अपवादांवर अनुप्रयोग थ्रो आणि क्रॅश करा. 5 क्रॅश नंतर रीसेट केले जाईल + पूर्णस्क्रीन सक्षम करा + स्क्रीनशॉट आणि रेकॉर्डिंग्जना अनुमती देण्यासाठी अॅपला पूर्णस्क्रीनमध्ये बनवा + नोट यूयूआयडी दर्शवा + मुख्यपृष्ठ स्क्रीन दृश्यात नोट्सचे अनन्य id दर्शवा + खोटे अपवाद + अपवाद वैशिष्ट्यांचे परीक्षण करण्यासाठी बनावट अपवाद थ्रो + अपवाद फेकले + क्रॅश अॅप + मेल + संपर्क खंडित + Google ड्राइव्ह वर समक्रमित करा + Google ड्राइव्ह आपल्याला आपले स्वत: चे Google ड्राइव्ह खाते वापरून डिव्हाइसेस दरम्यान आपली टिपा संकालित करण्याची परवानगी देते. + आधी Google फायरबेस मध्ये लॉग इन केले? + Google ड्राइव्ह मध्ये साइन इन करा + साइन इन करत आहे + स्कार्लेट बाहेर साइन आउट करा + नोट्स, टॅग आणि फोल्डर समक्रमित करणे थांबवा. + आपल्या Google ड्राइव्हवर आपले नोट्स, टॅग आणि फोल्डर समक्रमित करणे थांबवा. + आपला अॅप आणि Google ड्राइव्हवरील डेटा अद्याप तेथेच आहे. + ड्राइव्हमधून साइन आउट करा + साइन आउट करत आहे + नोट्स, टॅग्ज आणि फोल्डर्स आपल्या स्वत: च्या Google ड्राइव्हवर संग्रहित केले जातात, म्हणूनच आपण त्यांच्यामध्ये प्रवेश नियंत्रित करू शकता. + आपले फोटो देखील डिव्हाइसेसवर अपलोड आणि समक्रमित केले जातात. + फायरबेस पासून डेटा पुनर्संचयित करा + Google सह साइन इन करा + साइन इन करत आहे + आम्ही आपली माहिती Google फायरबेसवर संग्रहित करतो. लॉग इन केल्यानंतर आम्ही ते आपल्या डिव्हाइसवर पुनर्प्राप्त करू + नवीन नोट्स आणि बदल Google फायरबेसवर समक्रमित केले जाणार नाहीत आणि आपल्याला Google ड्राइव्ह वर लॉग इन करणे आवश्यक आहे + एकदा आपली नोट्स पुनर्प्राप्त झाल्यानंतर, आपण त्यांना Google फायरबेसमधून हटवू शकता आणि नंतर स्विच करू शकता + आपला डेटा समक्रमित केला जात नाही! + लीगेसी लॉगिनवर आधारित डेटा सिंक अक्षम केला आहे, Google ड्राइव्ह आधारित सिंकवर स्विच करणे विचारात घ्या. + फायरबेस पासून ड्राइव्हवर डेटा स्थानांतरित करा + डेटा साफ करा आणि साइन आउट करा + क्लिअरिंग आणि साइनिंग आउट + आम्ही आपली माहिती Google फायरबेसवर संग्रहित करतो. आम्ही आपल्या Google ड्राइव्हमध्ये आपले नोट्स संचयित करण्यासाठी पुढे जात आहोत. + अधिक गोपनीयता + फोटो समक्रमण + पुढची पायरी + आम्ही प्रथम आपल्या नोट्स फायरबेसमधून हटवू आणि आपल्याला लॉग आउट करू. + आपण नंतर Google ड्राइव्हमध्ये लॉग इन करू शकता आणि आम्ही आपला डेटा प्रतिमांसह अपलोड करू. + \ No newline at end of file diff --git a/base/src/main/res/values-ms/strings.xml b/base/src/main/res/values-ms/strings.xml index b62e372a..e1e9ec82 100644 --- a/base/src/main/res/values-ms/strings.xml +++ b/base/src/main/res/values-ms/strings.xml @@ -314,4 +314,97 @@ Padamkan nota, teg dan folder dalam aplikasi Latar Belakang Pencari Nota + + Merge Notes + Eksport Sebagai Penurunan Nilai + Nota eksport dalam format markdown. (Anda tidak boleh mengimport kembali ini ke aplikasi) + Folder Eksport + Pilih di mana semua eksport disimpan + Gunakan warna tema untuk latar belakang + Gunakan warna nota untuk latar belakang + Dayakan + Lumpuhkan + Dayakan Penyegerakan Folder + Penyegerakan Folder + Segerakkan semua nota, teg dan folder anda ke folder luaran. Ini membantu anda menyelaraskan menggunakan aplikasi lain antara peranti, dan juga mempunyai salinan sekiranya anda perlu memadamkan aplikasi. + Mengeksport ke Folder + Segerakkan semua nota, teg, dan lain-lain ke folder luaran + Nota terkunci eksport + Abjad + Boleh didapati di Scarlet Pro + Juga sandarkan nota yang terkunci + Pilihan Editor + Tukar tetapan dan penggunaan untuk editor nota + Pilihan Kemaskini sebagai Default + Tunjukkan butang cepat keluar pada bar alat sebagai lalai + Pengendalian Masa Nyata + Pilih jika anda mahu markdown dapat dilihat sebagai jenis anda. Mendayakan boleh memberi kesan pada nota besar. + Pindah Item Diperiksa + Item yang diperiksa bergerak ke bahagian bawah senarai. Nyahtanda tidak menetapkan semula kedudukan. + Tunjukkan Pergerakan Gerakan + Tunjukkan pemegang untuk memindahkan item ke atas atau ke bawah. Anda masih boleh memindahkan perkara dengan menyentuh sudut. + Contoh Markdown + Tetapan Widget + Tukar tetapan untuk widget skrin utama + Dayakan Pemformatan + Nota diberikan dengan memformat dalam widget skrin utama + Tunjukkan Nota Dikunci + Benarkan nota terkunci untuk ditunjukkan dalam widget skrin utama + Tunjukkan Nota Terarkib + Benarkan nota arkib akan ditunjukkan dalam widget skrin utama + Tunjukkan Nota dalam Sampah + Benarkan nota dalam sampah untuk ditunjukkan dalam widget skrin utama + Soalan Bantuan dan Biasa + Cari bantuan tentang cara menggunakan ciri dalam aplikasinya + Penyelarasan + Menunggu Cadangan + Pilihan Pemaju + Tukar tetapan dalaman ke aplikasi yang digunakan untuk penyahpepijatan + Pengecualian Log + Log tertangkap pengecualian ke nota tetap, untuk membenarkan penghantaran ke Pemaju + Paparkan Helaian Pengecualian + Tunjukkan pengecualian yang terperangkap pada lembaran jika boleh, untuk membenarkan penghantaran ke Pemaju + Buang pada Pengecualian + Buang dan kemalangan aplikasi pada pengecualian. Akan ditetapkan semula selepas 5 kemalangan + Dayakan Fullscreen + Jadikan aplikasinya di skrin penuh untuk membenarkan tangkapan skrin dan rakaman + Tunjukkan Nota UUIDs + Tunjukkan id unik nota dalam paparan skrin utama + Pengecualian Palsu + Buang pengecualian palsu untuk menguji ciri pengecualian + Pengecualian Dibuang + Crash App + Mel + Sambungan gagal + Segerakkan di Google Drive + Google Drive membolehkan anda menyegerakkan nota anda antara peranti menggunakan akaun Google Drive anda sendiri. + Log masuk ke Google Firebase sebelum ini? + Masuk ke Google Drive + Menandatangani Inâ € | + Keluar dari Scarlet + Berhenti menyegerakkan nota, teg dan folder. + Berhenti menyegerakkan nota, teg dan folder anda ke Google Drive anda. + Data anda di apl dan di Google Drive masih di sana. + Log keluar dari Drive + Menandatangani Outâ € | + Nota, Teg dan Folder disimpan di Google Drive anda sendiri, jadi hanya anda yang boleh mengawal akses kepada mereka. + Foto anda dimuat naik dan disegerakkan merentasi peranti juga. + Pulihkan Data daripada Firebase + Masuk dengan Google + Menandatangani Inâ € | + Kami digunakan untuk menyimpan maklumat anda di Google Firebase. Selepas log masuk, kami akan memulihkannya ke peranti anda + Nota dan perubahan baru TIDAK akan diselaraskan ke Google Firebase, dan anda perlu log masuk ke Google Drive + Apabila nota anda pulih, anda boleh memadamkannya dari Google Firebase, dan kemudian beralih + Data anda tidak disegerakkan! + Penyegerakan Data berdasarkan log masuk lama dilumpuhkan, pertimbangkan untuk bertukar ke penyegerakan berasaskan Google Drive. + Memindahkan Data dari Firebase ke Drive + Kosongkan Data dan Log Keluar + Penjelasan dan Keluar + Kami digunakan untuk menyimpan maklumat anda di Google Firebase. Kami sedang bergerak untuk menyimpan nota anda di Google Drive anda. + Privasi lebih lanjut. + Penyegerakan Foto. + Langkah seterusnya + Kami akan memadamkan nota anda dari Firebase dan log keluar anda terlebih dahulu. + Anda kemudiannya boleh log masuk ke Google Drive, dan kami akan memuat naik data anda bersama-sama dengan imej. + \ No newline at end of file diff --git a/base/src/main/res/values-pa/strings.xml b/base/src/main/res/values-pa/strings.xml index 0d472825..80b5a3ee 100644 --- a/base/src/main/res/values-pa/strings.xml +++ b/base/src/main/res/values-pa/strings.xml @@ -316,4 +316,97 @@ ਐਪ ਵਿੱਚ ਨੋਟਸ, ਟੈਗਸ ਅਤੇ ਫੋਲਡਰ ਮਿਟਾਓ ਨੋਟ ਵਿਊਅਰ ਬੈਕਗਰਾਊਂਡ + + ਨੋਟਸ ਮਿਲਾਓ + ਮਾਰਕਡਾਉਨ ਦੇ ਤੌਰ ਤੇ ਨਿਰਯਾਤ ਕਰੋ + ਮਾਰਕੇਡਾਊਨ ਫਾਰਮੈਟ ਵਿੱਚ ਨੋਟ ਨਿਰਯਾਤ ਕਰੋ. (ਤੁਸੀਂ ਇਹ ਵਾਪਸ ਐਪ ਤੇ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕਦੇ) + ਫੋਲਡਰ ਨਿਰਯਾਤ ਕਰੋ + ਚੁਣੋ ਕਿ ਸਾਰੇ ਨਿਰਯਾਤ ਕਿੱਥੇ ਸਟੋਰ ਕੀਤੇ ਜਾਂਦੇ ਹਨ + ਪਿਛੋਕੜ ਲਈ ਥੀਮ ਕਲੰਡ ਵਰਤੋਂ + ਪਿਛੋਕੜ ਲਈ ਨੋਟ ਰੰਗ ਦੀ ਵਰਤੋਂ ਕਰੋ + ਸਮਰੱਥ ਬਣਾਓ + ਅਸਮਰੱਥ ਕਰੋ + ਫੋਲਡਰ ਸਿੰਕ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ + ਫੋਲਡਰ ਸਿੰਕ ਕਰੋ + ਆਪਣੇ ਸਾਰੇ ਨੋਟਸ, ਟੈਗਸ ਅਤੇ ਫੋਲਡਰ ਨੂੰ ਇੱਕ ਬਾਹਰੀ ਫੋਲਡਰ ਵਿੱਚ ਸਿੰਕ ਕਰੋ. ਇਹ ਡਿਵਾਈਸਾਂ ਦੇ ਵਿੱਚਕਾਰ ਦੂਜੀਆਂ ਐਪਲੀਕੇਸ਼ਾਂ ਦਾ ਉਪਯੋਗ ਕਰਕੇ ਸਿੰਕ ਕਰਨ ਵਿੱਚ ਤੁਹਾਡੀ ਸਹਾਇਤਾ ਕਰਦਾ ਹੈ, ਨਾਲ ਹੀ ਤੁਹਾਡੇ ਕੋਲ ਐਪ ਨੂੰ ਮਿਟਾਉਣ ਦੀ ਜ਼ਰੂਰਤ ਦੇ ਨਾਲ ਇੱਕ ਕਾਪੀ ਹੈ + ਫੋਲਡਰ ਵਿੱਚ ਐਕਸਪੋਰਟ ਕਰ ਰਿਹਾ ਹੈ + ਸਾਰੇ ਨੋਟਸ, ਟੈਗਾਂ ਆਦਿ ਨੂੰ ਇੱਕ ਬਾਹਰੀ ਫੋਲਡਰ ਵਿੱਚ ਸਿੰਕ ਕਰੋ + ਤਾਲਾਬੰਦ ਨੋਟ ਐਕਸਪੋਰਟ + ਵਰਣਮਾਲਾ ਦੇ ਅਨੁਸਾਰ + ਸਕਾਰਲੈਟ ਪ੍ਰੋ ਤੇ ਉਪਲਬਧ + ਲੌਕ ਹੋਏ ਨੋਟਸ ਨੂੰ ਵੀ ਬੈਕਅਪ ਕਰੋ + ਸੰਪਾਦਕ ਵਿਕਲਪ + ਨੋਟ ਸੰਪਾਦਕ ਲਈ ਸੈਟਿੰਗਾਂ ਅਤੇ ਵਰਤੋਂ ਬਦਲੋ + Markdown ਚੋਣਾਂ ਨੂੰ ਡਿਫਾਲਟ ਦੇ ਤੌਰ ਤੇ + ਡਿਫੌਲਟ ਵਜੋਂ ਟੂਲਬਾਰ ਤੇ ਮਾਰਕਡਾਊਨ ਤੇਜ਼ ਬਟਨ ਦਿਖਾਓ + ਰੀਅਲਟਾਈਮ ਮਾਰਕਾਡਾਊਨ + ਚੁਣੋ ਕਿ ਕੀ ਤੁਸੀਂ ਚਾਹੁੰਦੇ ਹੋ ਕਿ ਮਾਰਕਡਾਊਨ ਤੁਹਾਡੀ ਕਿਸਮ ਦੇ ਤੌਰ ਤੇ ਦਿਖਾਈ ਦੇਵੇ ਯੋਗ ਕਰਨਾ ਵੱਡੇ ਨੋਟਸ ਤੇ ਪ੍ਰਦਰਸ਼ਨ ਨੂੰ ਪ੍ਰਭਾਵਤ ਕਰ ਸਕਦਾ ਹੈ. + ਚੈਕਡ ਆਈਟਮਾਂ ਨੂੰ ਮੂਵ ਕਰੋ + ਚੈੱਕ ਕੀਤੀਆਂ ਆਈਟਮਾਂ ਸੂਚੀ ਦੇ ਹੇਠਾਂ ਚਲੀਆਂ ਗਈਆਂ ਹਨ ਅਨਚੈਕ ਪੁਜ਼ੀਸ਼ਨ ਨੂੰ ਰੀਸੈਟ ਨਹੀਂ ਕਰਦਾ. + ਅੰਦੋਲਨ ਹੈਂਡਲ ਵੇਖੋ + ਆਈਟਮਾਂ ਨੂੰ ਉੱਪਰ ਜਾਂ ਹੇਠਾਂ ਵੱਲ ਹਿਲਾਉਣ ਲਈ ਹੈਂਡਲ ਦਿਖਾਓ ਤੁਸੀਂ ਅਜੇ ਵੀ ਕੋਨੇ ਨੂੰ ਛੂਹ ਕੇ ਆਲੇ ਦੁਆਲੇ ਦੀਆਂ ਚੀਜਾਂ ਨੂੰ ਪ੍ਰਭਾਵਿਤ ਕਰ ਸਕਦੇ ਹੋ + ਮਾਰਕੇਡਾਉਨ ਦੀਆਂ ਉਦਾਹਰਨਾਂ + ਵਿਜੇਟ ਸੈਟਿੰਗਜ਼ + ਹੋਮ ਸਕ੍ਰੀਨ ਵਿਜੇਟਸ ਲਈ ਸੈਟਿੰਗਾਂ ਬਦਲੋ + ਫਾਰਮੈਟਿੰਗ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ + ਹੋਮ ਸਕ੍ਰੀਨ ਵਿਡਜਿਟ ਵਿੱਚ ਫੌਰਮੈਟਿੰਗ ਦੇ ਨਾਲ ਨੋਟਸ ਰੈਂਡਰ ਕੀਤੇ ਜਾਂਦੇ ਹਨ + ਲੌਕਡ ਨੋਟਸ ਦਿਖਾਓ + ਲੌਕ ਕੀਤੀਆਂ ਨੋਟਾਂ ਨੂੰ ਹੋਮ ਸਕ੍ਰੀਨ ਵਿਜੇਟਾਂ ਵਿੱਚ ਦਿਖਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ + ਆਰਕਾਈਵਡ ਨੋਟਸ ਵੇਖੋ + ਆਰਕਾਈਵ ਕੀਤੀਆਂ ਸੂਚਨਾਵਾਂ ਨੂੰ ਹੋਮ ਸਕ੍ਰੀਨ ਵਿਜੇਟਾਂ ਵਿੱਚ ਦਿਖਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ + ਰੱਦੀ ਵਿੱਚ ਨੋਟ ਵੇਖੋ + ਘਰਾਂ ਦੀਆਂ ਸਕ੍ਰੀਨ ਵਿਜੇਟਾਂ ਵਿੱਚ ਰੱਦੀ ਵਿੱਚ ਨੋਟਸ ਦਿਖਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ + ਮਦਦ ਅਤੇ ਆਮ ਸਵਾਲ + ਐਪ ਵਿੱਚ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਦਾ ਉਪਯੋਗ ਕਿਵੇਂ ਕਰਨਾ ਹੈ ਬਾਰੇ ਮਦਦ ਲੱਭੋ + ਸਿੰਕ ਕਰਨਾ + ਬਕਾਇਆ ਬੈਕਅਪ + ਵਿਕਾਸਕਾਰ ਵਿਕਲਪ + ਡੀਬੱਗਿੰਗ ਲਈ ਵਰਤੀ ਜਾਂਦੀ ਐਪਲੀਕੇਸ਼ਨ ਵਿੱਚ ਅੰਦਰੂਨੀ ਸੈਟਿੰਗ ਬਦਲੋ + ਲਾਗ ਅਪਵਾਦ + ਡਿਵੈਲਪਰ ਨੂੰ ਫਾਰਵਰਡਿੰਗ ਦੀ ਆਗਿਆ ਦੇਣ ਲਈ ਇੱਕ ਨਿਸ਼ਚਿਤ ਨੋਟ ਦੇ ਫਰਕ ਅਪਵਾਦ + ਅਪਵਾਦ ਸ਼ੀਟ ਦਿਖਾਓ + ਜੇ ਹੋ ਸਕੇ ਤਾਂ ਇੱਕ ਸ਼ੀਟ \'ਤੇ ਫੜੇ ਅਪਵਾਦ ਦਿਖਾਓ, ਤਾਂ ਕਿ ਡਿਵੈਲਪਰ ਨੂੰ ਫਾਰਵਰਡਿੰਗ ਦੀ ਆਗਿਆ ਦਿੱਤੀ ਜਾ ਸਕੇ + ਅਪਵਾਦ ਤੇ ਸੁੱਟੋ + ਅਪਵਾਦ ਤੇ ਅਰਜ਼ੀ ਨੂੰ ਸੁੱਟ ਅਤੇ ਕਰੈਸ਼ ਕਰੋ. 5 ਕ੍ਰੈਸ਼ਾਂ ਦੇ ਬਾਅਦ ਰੀਸੈਟ ਕੀਤਾ ਜਾਏਗਾ + ਫੁਲਸਕ੍ਰੀਨ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ + ਸਕ੍ਰੀਨਸ਼ਾਟ ਅਤੇ ਰਿਕਾਰਡਿੰਗਾਂ ਦੀ ਆਗਿਆ ਦੇਣ ਲਈ ਐਪ ਨੂੰ ਫੁਲਸਕ੍ਰੀਨ ਬਣਾਉ + ਨੋਟ ਵੇਖੋ UUID + ਹੋਮ ਸਕ੍ਰੀਨ ਵਿਯੂ ਵਿੱਚ ਨੋਟਸ ਦੀਆਂ ਵਿਲੱਖਣ ਆਈਡੀਆਂ ਦਿਖਾਓ + ਨਕਲੀ ਅਪਵਾਦ + ਅਪਵਾਦ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਦੀ ਜਾਂਚ ਕਰਨ ਲਈ ਇੱਕ ਨਕਲੀ ਅਪਵਾਦ ਨੂੰ ਸੁੱਟੋ + ਅਪਵਾਦ ਸੁੱਟਿਆ + ਕ੍ਰੈਸ਼ ਐਪ + ਮੇਲ + ਕੁਨੈਕਸ਼ਨ ਫੇਲ੍ਹ ਹੈ + Google Drive ਤੇ ਸਿੰਕ ਕਰੋ + Google ਡ੍ਰਾਇਵ ਤੁਹਾਨੂੰ ਤੁਹਾਡੀਆਂ ਖੁਦ ਦੀ Google ਡ੍ਰਾਈਵ ਖਾਤੇ ਦੀ ਵਰਤੋਂ ਕਰਦੇ ਹੋਏ ਡਿਵਾਈਸਾਂ ਦੇ ਵਿਚਕਾਰ ਆਪਣੇ ਨੋਟਸ ਨੂੰ ਸਿੰਕ ਕਰਨ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ. + Google Firebase ਵਿੱਚ ਪਹਿਲਾਂ ਲੌਗਇਨ ਕੀਤਾ? + Google Drive ਵਿੱਚ ਸਾਈਨ ਇਨ ਕਰੋ + ਸਾਈਨ ਇਨ ਕਰੋ | + ਸਕਾਰਲੇਟ ਦੇ ਸਾਈਨ ਆਊਟ + ਨੋਟਸ, ਟੈਗਸ ਅਤੇ ਫੋਲਡਰਸਿੰਗ ਸਿੰਕਿੰਗ ਬੰਦ ਕਰੋ + ਆਪਣੇ Google Drive ਤੇ ਤੁਹਾਡੇ ਨੋਟਸ, ਟੈਗਸ ਅਤੇ ਫੋਲਡਰਸ ਨੂੰ ਸਿੰਕ ਕਰਨਾ ਬੰਦ ਕਰੋ + ਐਪ ਅਤੇ Google ਡਰਾਇਵ \'ਤੇ ਤੁਹਾਡਾ ਡੇਟਾ ਅਜੇ ਵੀ ਉੱਥੇ ਹੋਵੇਗਾ + ਡ੍ਰਾਈਵ ਤੋਂ ਸਾਈਨ ਆਉਟ ਕਰੋ + ਸਾਈਨ ਆਉਟ \'| + ਨੋਟਸ, ਟੈਗਸ ਅਤੇ ਫੋਲਡਰ ਤੁਹਾਡੀ ਆਪਣੀ ਗੂਗਲ ਡ੍ਰਾਇਵ ਉੱਤੇ ਸਟੋਰ ਕੀਤੇ ਜਾਂਦੇ ਹਨ, ਇਸ ਲਈ ਸਿਰਫ ਤੁਸੀਂ ਉਹਨਾਂ ਤੱਕ ਪਹੁੰਚ ਨੂੰ ਕੰਟਰੋਲ ਕਰ ਸਕਦੇ ਹੋ. + ਤੁਹਾਡੀਆਂ ਫੋਟੋਆਂ ਨੂੰ ਵੀ ਡਿਵਾਈਸ ਉੱਤੇ ਅਪਲੋਡ ਅਤੇ ਸਿੰਕ ਕੀਤਾ ਗਿਆ ਹੈ + ਫਾਇਰਬੇਸ ਤੋਂ ਡਾਟਾ ਰੀਸਟੋਰ ਕਰੋ + Google ਦੇ ਨਾਲ ਸਾਈਨ ਇਨ ਕਰੋ + ਸਾਈਨ ਇਨ ਕਰੋ | + ਅਸੀਂ ਤੁਹਾਡੀ ਜਾਣਕਾਰੀ ਨੂੰ Google Firebase ਤੇ ਸਟੋਰ ਕਰਨ ਲਈ ਵਰਤਿਆ ਸੀ ਲੌਗਇਨ ਕਰਨ ਦੇ ਬਾਅਦ ਅਸੀਂ ਇਸਨੂੰ ਤੁਹਾਡੀ ਡਿਵਾਈਸ ਤੇ ਵਾਪਸ ਪ੍ਰਾਪਤ ਕਰਾਂਗੇ + ਨਵੇਂ ਨੋਟਸ ਅਤੇ ਬਦਲਾਵ Google Firebase ਤੇ ਸਿੰਕ ਨਹੀਂ ਕੀਤੇ ਜਾਣਗੇ, ਅਤੇ ਤੁਹਾਨੂੰ Google Drive ਤੇ ਲਾਗਇਨ ਕਰਨ ਦੀ ਲੋੜ ਹੈ + ਇੱਕ ਵਾਰ ਜਦੋਂ ਤੁਹਾਡੇ ਨੋਟਸ ਰਿਕਵਰ ਕੀਤੇ ਜਾਂਦੇ ਹਨ, ਤੁਸੀਂ ਉਹਨਾਂ ਨੂੰ Google Firebase ਤੋਂ ਮਿਟਾ ਸਕਦੇ ਹੋ, ਅਤੇ ਫਿਰ ਇਸਦੇ ਬਦਲੀ ਕਰ ਸਕਦੇ ਹੋ + ਤੁਹਾਡੇ ਡੇਟਾ ਨੂੰ ਸਿੰਕ ਨਹੀਂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ! + ਲੇਗਸੀ ਲੌਗਿਨ ਦੇ ਅਧਾਰ \'ਤੇ ਡੇਟਾ ਸਿੰਕ ਅਸਮਰੱਥ ਹੈ, Google ਡ੍ਰਾਇਵ ਆਧਾਰਿਤ ਸਿੰਕ ਤੇ ਸਵਿਚ ਕਰਨ ਦਾ ਵਿਚਾਰ ਕਰੋ. + ਫਾਇਰਬੇਸ ਤੋਂ ਡਰਾਇਵ ਤੱਕ ਡੇਟਾ ਟ੍ਰਾਂਸਫਰ ਕਰੋ + ਡਾਟਾ ਸਾਫ਼ ਕਰੋ ਅਤੇ ਸਾਈਨ ਆਉਟ ਕਰੋ + ਕਲੀਅਰਿੰਗ ਅਤੇ ਸਾਈਨ ਆਉਟ + ਅਸੀਂ ਤੁਹਾਡੀ ਜਾਣਕਾਰੀ ਨੂੰ Google Firebase ਤੇ ਸਟੋਰ ਕਰਨ ਲਈ ਵਰਤਿਆ ਸੀ ਅਸੀਂ ਤੁਹਾਡੇ Google ਡਰਾਈਵ ਵਿੱਚ ਆਪਣੇ ਨੋਟਸ ਨੂੰ ਸਟੋਰ ਕਰਨ ਲਈ ਅੱਗੇ ਵਧ ਰਹੇ ਹਾਂ. + ਹੋਰ ਗੋਪਨੀਯਤਾ + ਫੋਟੋ ਸਮਕਾਲੀ + ਅਗਲਾ ਕਦਮ + ਅਸੀਂ ਪਹਿਲਾਂ ਫਾਇਰਬੇਜ ਤੋਂ ਤੁਹਾਡੇ ਨੋਟਸ ਮਿਟਾ ਸਕਦੇ ਹਾਂ ਅਤੇ ਤੁਹਾਨੂੰ ਆਉਟ ਕਰ ਸਕਦੇ ਹਾਂ. + ਤੁਸੀਂ ਫਿਰ Google Drive ਤੇ ਲੌਗ ਇਨ ਕਰ ਸਕਦੇ ਹੋ, ਅਤੇ ਅਸੀਂ ਚਿੱਤਰਾਂ ਦੇ ਨਾਲ ਤੁਹਾਡੇ ਡੇਟਾ ਨੂੰ ਅਪਲੋਡ ਕਰਾਂਗੇ. + \ No newline at end of file diff --git a/base/src/main/res/values-pl/strings.xml b/base/src/main/res/values-pl/strings.xml new file mode 100644 index 00000000..a8489a60 --- /dev/null +++ b/base/src/main/res/values-pl/strings.xml @@ -0,0 +1,390 @@ + + + + Tytuł + Zawartość + Zapisać + Import + Szukaj w notatkach… + Szukaj %s… + Udostępnij za pomocą… + Brak notatek + Wygląda na to, że nie dodałeś żadnych notatek. Kliknij, aby dodać notatkę. + Dodaj notatkę… + Dodaj szybką notatkę… + Dodaj notatkę + Dodaj listę kontrolną + Tryb nocny + Tryb dzienny + Edytuj notatkę + Utwórz powiadomienie + Otwórz w Popup + Usuń notatkę + Usuń trwale + Kopiuj notatkę + Wyślij notatkę + Unarchive Note + Notatka archiwalna + Oznacz jako ulubione + Oznacz Ulubione + Przywróć notatkę + Przenieść do kosza + Zmień kolor + Uwaga blokady + Zmień tagi + Odblokuj notatkę + Przypomnienie + Wybierz + Duplikować + Przypnij notatkę + Odepnij notatkę + Distraction Free + Scal notatki + Wybierz działanie… + Dotknij, aby wyświetlić notatkę w trybie nocnym + Dotknij, aby wyświetlić notatkę w trybie dziennym + Dotknij, aby otworzyć notatkę do edycji + Dotknij, aby skopiować zawartość notatki do schowka + Dotknij, aby udostępnić treść notatki innym aplikacjom + Dotknij, aby otworzyć notatkę w wyskakującym okienku + Dotknij, aby usunąć notatkę + Dotknij, aby przenieść notatkę do kosza + Dotknij, aby przenieść notatkę z kosza + Dotknij, aby zaznaczyć notatkę jako ulubioną + Dotknij, aby zaznaczyć notatkę nie jako ulubioną + Wybierz, aby zarchiwizować notatkę + Dotknij, aby zmienić kolor tła notatki + Dotknij, aby rozpakować notatkę + Twoje notatki i więcej… + Dom + Wszystkie twoje normalne i ulubione notatki + Ulubione + Wszystkie twoje ulubione notatki + Zarchiwizowane + Wszystkie zarchiwizowane notatki + Zablokowany + Wszystkie twoje zablokowane notatki + Tagi + Wszystkie twoje tagi notatek + Śmieci + Wszystkie twoje notatki w koszu + Dodaj treść… + Dodaj nagłówek… + Dodaj podpozycję… + Dodaj cytowany tekst… + Dodaj element… + Dodaj kod… + Kliknij, aby dodać lub zmienić obraz + Formatowanie bloku + Nagłówek + Podtytuł + Tekst + Zacytować + Kod + Lista kontrolna + Obraz + Separator + Formatowanie w dół + Pogrubienie + Kursywa + Podkreślać + Strajk + Lista + + + Opcje i ustawienia + Interfejs i doświadczenie + Wybierz sposób, w jaki aplikacja wygląda i czuje się. + Preferencje notatki + Wybierz notatkę i inne ustawienia. + O + Dowiedz się więcej o nas i aplikacji. + Bezpieczeństwo + Blokowanie notatek i opcji zabezpieczeń + Włącz tryb nocny + Włącz ciemny motyw jako domyślny + Włącz tryb dzienny + Włącz domyślny motyw światła + Ustawienia przeceny + Zobacz, jak używać notowań w notowaniach + Włącz układ listy + Pokaż notatki w jednej kolumnie + Włącz układ siatki + Pokaż notatki w rozłożonej siatce + Uwagi dotyczące zamówień + Kopia zapasowa i import + Opcje tworzenia kopii zapasowych, importowania i eksportowania notatek + Eksportuj notatki + Eksportuj notatki do pamięci urządzenia, aby udostępnić + Importuj notatki + Importuj notatki z pamięci urządzenia + Eksportuj jako Markdown + Eksportuj notatki w formacie przeceny. (Nie możesz zaimportować ich z powrotem do aplikacji) + Eksportuj folder + Wybierz miejsce przechowywania wszystkich eksportów + Eksportuj automatycznie + Często eksportuj notatki do pliku zewnętrznego jako kopię zapasową + O nas + Dowiedz się więcej o aplikacji i programistach + Projekt Open Source + Dowiedz się więcej o projekcie open source + Oceń i zrecenzuj + Powiedz nam, ile podobała ci się aplikacja + Wypełnij ankietę + Pomóż nam ulepszyć aplikację, mówiąc nam, co lubisz + Zainstaluj Pro App + Zainstaluj Pro App, aby odblokować funkcje i wsparcie programisty + Migracja notatek do Scarlet Pro + Przeprowadź migrację wszystkich notatek do aplikacji pro + Usuń notatki i więcej + Usuń notatki, tagi i inne dane w aplikacji + Usuń wszystkie notatki + Usuń wszystkie notatki z aplikacji + Usuń wszystkie foldery + Usuń wszystkie foldery w aplikacji + Usuń wszystkie tagi + Usuń wszystkie tagi w aplikacji + Usuń wszystko + Usuń notatki, tagi i foldery w aplikacji + Tło podglądu notatki + Użyj koloru motywu dla tła + Użyj koloru notatki dla tła + + + Opcje ochrony + Kod dostępu + Ustaw 4-cyfrowy kod PIN, aby zablokować notatki + Odblokuj za pomocą odcisku palca + Notatki zostaną odblokowane za pomocą odcisku palca + Odcisk palca wyłączony + Notatki nie wykorzystują odcisku palca + Wprowadź nowy kod PIN + Wprowadź kod PIN, aby odblokować + Wprowadź bieżący kod PIN + Wprowadź kod + Zweryfikować + Odblokować + Zestaw + Usunąć + Domyślny kolor notatki + Wybierz domyślny kolor nut + Limit linii uwagi + %d linie na ekranie głównym + Wsparcie w przypadku obniżki cen + Zezwalaj na formatowanie obsługiwane przez przecenę + Markdown in Note List + Włącz formatowanie Markdown na liście notatek + Przykłady + Daj uprawnienia + Importowanie i eksportowanie wymaga uprawnień do przechowywania. Proszę o wyrażenie zgody na żądanie. + Dopuszczać + Importuj kopię zapasową z pliku + Importować plik + Eksportowane do pliku + Eksport nie powiódł się + Eksportowanie do pliku… + Gotowe + Dzielić + Włączyć + Wyłączyć + Włącz synchronizację folderów + Synchronizacja folderów + Synchronizuj wszystkie swoje notatki, tagi i foldery z folderem zewnętrznym. Pomaga to synchronizować za pomocą innych aplikacji między urządzeniami, a także mieć kopię na wypadek konieczności usunięcia aplikacji. + Eksportowanie do folderu + Zsynchronizuj wszystkie notatki, tagi itp. Z folderem zewnętrznym + Eksportuj zablokowane notatki + Wykonaj również kopię zapasową zablokowanych notatek + O nas i więcej + Oceń na Play Store + Przyczynić się + Wersja aplikacji + O aplikacji + Biblioteki + Projekt Open Source + O projekcie Open Source + Uwaga Przypomnienie + Data przypomnienia + Czas przypomnienia + Powtórz częstotliwość + Tylko raz + Codziennie + Zwyczaj + Beta + Uwaga Powiadomienia + Przypomnienia i alarmy + %s jest open source i każdy jest otwarty, aby przyczynić się do jego ulepszenia. Jest obecnie budowany i utrzymywany przez %s. + %s to prosta aplikacja do robienia notatek. Pozwala na szybkie wprowadzanie tekstu sformatowanego, nie czyniąc tego bardzo trudnym w użyciu. Sprawia, że ​​wielozadaniowość jest prosta. + Cześć, jesteśmy parą projektantów i programistów, którzy stworzyli %s. + Staramy się budować piękne, starannie zaprojektowane darmowe, wolne od reklam lub minimalne aplikacje reklamowe, które są przydatne dla wszystkich! + Dodaj nowy tag… + Edytuj znacznik… + wprowadź tag + + + Utwórz nowy tag + Wybierz tag… + Sortuj notatki według + Najpierw najnowsze + Najpierw najstarsi + Ostatnio zmodyfikowane + Alfabetyczny + Wybierz notatkę + Wybierz Notatki + Ustawienia + Uwaga usunięta lub zablokowana + Co nowego + Dowiedz się, co nowego w najnowszych aktualizacjach aplikacji + Tłumaczyć + Kliknij, aby dodać nową notatkę + Kliknij, aby zobaczyć opcje domu + Menu główne zawiera ulubione i archiwalne notatki, a także ustawienia aplikacji. Tutaj możesz znaleźć swoje tagi. + Kopiuj + Blokuj akcje tekstowe + Galeria + Aparat fotograficzny + Mów głośno na głos + Mówić na głos + Usunięcie tego elementu spowoduje usunięcie obrazu ze wszystkich urządzeń. + Obraz nie jest na tym urządzeniu + Nie można załadować obrazu + Obrazy nie są synchronizowane + Obrazy nie są synchronizowane między urządzeniami. Twoje obrazy nie będą wyświetlane na innych urządzeniach podczas wyświetlania notatki! + Rozumiem + Uwaga Rozmiar czcionki + Dostosuj rozmiar czcionki tekstu na stronie notatek. Możesz zobaczyć, jak będzie wyglądać w tym podglądzie. + %d rozmiar tekstu w przeglądarce notatek + Zainstaluj ze sklepu + Zainstaluj aplikację z Google Store dla Cloud Sync + Brak kodu PIN + Nie skonfigurowałeś kodu PIN. Czy chcesz to teraz skonfigurować? + Później + Nigdy nie pytaj + Ustawiać + Jesteś pewny? + Czy chcesz trwale usunąć notatki z folderu kosza? + Czy chcesz trwale usunąć tę notatkę? + Kasować + Anuluj + Notatki są usuwane na zawsze po 7 dniach + Informacja + Wybierz, aby zainstalować najnowszą wersję aplikacji + Wybierz, aby uaktualnić do Scarlet Pro + Zaloguj się do aplikacji + Zaloguj się, aby wykonać kopię zapasową w chmurze i synchronizację + Wyloguj się + Wyloguj się, aby zatrzymać tworzenie kopii zapasowej w chmurze + Zaloguj się za pomocą Google + Podpisywanie… + Jesteś zalogowany + Logowanie Google nie powiodło się + Polityka prywatności + Polityka prywatności aplikacji dla treści + Zainstaluj Pro App + Dostępne na Scarlet Pro + Dlaczego warto zainstalować Pro? + ✔ Wspieraj programistę w zakresie ogromnych kosztów serwera, aby uruchomić synchronizację w chmurze \n\n✔ Najpierw zdobądź najnowsze funkcje\n\n✔ Niektóre dodatkowe funkcje będą dostępne tylko dla użytkowników Pro + Dlaczego warto włączyć Cloud Sync + ✔ Przesyłanie i tworzenie kopii zapasowych przed zmianami urządzeń \n\n✔ Synchronizacja pomiędzy wieloma urządzeniami i szybkie aktualizacje \n\n✔ Bezpieczne zapisywanie na serwerach Google Firebase + + + Dzień dobry, + Notatki materiałowe są teraz Scarlet! + Zaczynać + Motyw aplikacji + Wybierz kolor tła dla motywu + Wybierz motyw aplikacji + Kliknij, aby dodać lub zmienić tagi + Pokaż więcej działań + Wybierz Notatki + Wybierz i wykonaj czynności na wielu notatkach jednocześnie + Notatka została przeniesiona do kosza + Notatka została usunięta + Cofnij + Wyłącz kopię zapasową + Włącz kopię zapasową + Pusty notatnik + 1 Uwaga + %d Notatki + Utwórz nowy notatnik + Edytuj notatnik + Dodaj notatnik + Dodaj do Notatnika + Zmień notatnik + Ostatnie notatki + Opcje edytora + Zmień ustawienia i użycie edytora notatek + Opcje wyboru jako domyślne + Pokaż domyślne przyciski szybkiego wybierania na pasku narzędzi + Markdown w czasie rzeczywistym + Wybierz, czy chcesz, aby przecena była widoczna jako twój typ. Włączenie może wpływać na wydajność dużych nut. + Przenieś zaznaczone przedmioty + Zaznaczone elementy przechodzą na dół listy. Usuń zaznaczenie nie resetuje pozycji. + Pokaż uchwyty ruchu + Pokaż uchwyt, aby przesunąć elementy w górę lub w dół. Nadal możesz przemieszczać rzeczy, dotykając rogu. + Przykłady obniżek + Ustawienia widżetu + Zmień ustawienia widżetów ekranu głównego + Włącz formatowanie + Notatki są renderowane z formatowaniem w widżetach ekranu głównego + Pokaż zablokowane notatki + Zezwalaj na wyświetlanie zablokowanych notatek w widżetach ekranu głównego + Pokaż zarchiwizowane notatki + Pozwól, aby zarchiwizowane notatki były wyświetlane w widżetach ekranu głównego + Pokaż notatki w koszu + Pozwól na wyświetlanie notatek w koszu na widżetach ekranu głównego + Pomoc i często zadawane pytania + Znajdź pomoc dotyczącą korzystania z funkcji w aplikacji + Synchronizacja + Oczekiwanie na kopię zapasową + Opcje programistyczne + Zmień ustawienia wewnętrzne na aplikację używaną do debugowania + Wyjątki dziennika + Zarejestruj wychwycone wyjątki w ustalonej notatce, aby umożliwić przekazywanie do programisty + + + Pokaż arkusz wyjątków + Pokaż wychwycone wyjątki na arkuszu, jeśli to możliwe, aby umożliwić przekazanie do programisty + Wyrzuć wyjątki + Rzucaj i zawieszaj aplikację na wyjątkach. Zostanie zresetowany po 5 awariach + Włącz pełny ekran + Ustaw aplikację na pełnym ekranie, aby umożliwić zrzuty ekranu i nagrania + Pokaż identyfikatory UUID + Pokaż unikalne identyfikatory notatek w widoku ekranu głównego + Fałszywe wyjątki + Wyrzuć fałszywy wyjątek, aby przetestować funkcje wyjątku + Wyjątek rzucony + Crash App + Poczta + Połączenie nieudane + Synchronizuj na Dysku Google + Dysk Google umożliwia synchronizację notatek między urządzeniami za pomocą własnego konta Dysku Google. + Zalogowałeś się wcześniej do Google Firebase? + Zaloguj się na Dysk Google + Podpisywanie… + Wyloguj się z Scarlet + Przestań synchronizować notatki, tagi i foldery. + Przestań synchronizować swoje notatki, tagi i foldery z Dyskiem Google. + Twoje dane w aplikacji i na Dysku Google nadal tam będą. + Wyloguj się z dysku + Wylogowanie… + Notatki, tagi i foldery są przechowywane na własnym Dysku Google, więc tylko Ty możesz kontrolować dostęp do nich. + Twoje zdjęcia są przesyłane i synchronizowane również na różnych urządzeniach. + Przywróć dane z Firebase + Zaloguj się za pomocą Google + Podpisywanie… + Używaliśmy do przechowywania twoich informacji w Google Firebase. Po zalogowaniu odzyskamy je na Twoim urządzeniu + Nowe notatki i zmiany NIE zostaną zsynchronizowane z Google Firebase i musisz zalogować się na Dysk Google + Po odzyskaniu notatek możesz je usunąć z Google Firebase, a następnie przełączyć + Twoje dane nie są synchronizowane! + Synchronizacja danych oparta na starszym logowaniu jest wyłączona, należy rozważyć przejście na synchronizację opartą na Dysku Google. + Przenieś dane z Firebase do napędu + Wyczyść dane i wyloguj się + Czyszczenie i wylogowywanie + Używaliśmy do przechowywania twoich informacji w Google Firebase. Przechodzimy do przechowywania notatek na Twoim Dysku Google. + Więcej prywatności. + Synchronizacja zdjęć. + Następne kroki + Najpierw usuniemy notatki z Firebase i wylogujemy się. + Następnie możesz zalogować się na Dysk Google, a my prześlemy Twoje dane wraz z obrazami. + + \ No newline at end of file diff --git a/base/src/main/res/values-pt/strings.xml b/base/src/main/res/values-pt/strings.xml index 9e0fb215..bf69df6a 100644 --- a/base/src/main/res/values-pt/strings.xml +++ b/base/src/main/res/values-pt/strings.xml @@ -317,4 +317,97 @@ Excluir notas, tags e pastas no aplicativo Fundo do visualizador de nota + + Mesclar anotações + Exportar como Markdown + Exportar notas no formato de marcação. (Você não pode importar de volta para o aplicativo) + Pasta de Exportação + Escolha onde todas as exportações são armazenadas + Use a cor do tema para o fundo + Use a cor da nota para o fundo + Habilitar + Desabilitar + Ativar sincronização de pastas + Sincronização de pastas + Sincronize todas as suas anotações, tags e pastas em uma pasta externa. Isso ajuda você a sincronizar usando outros aplicativos entre dispositivos, além de ter uma cópia no caso de precisar excluir o aplicativo. + Exportando para pasta + Sincronize todas as notas, tags, etc para uma pasta externa + Exportar notas bloqueadas + Alfabético + Disponível no Scarlet Pro + Também faça backup das notas bloqueadas + Opções do editor + Alterar as configurações e uso para o editor de notas + Opções de marcação como padrão + Mostrar os botões rápidos de markdown na barra de ferramentas como padrão + Markdown em tempo real + Escolha se você deseja que o markdown seja visível como seu tipo. A ativação pode afetar o desempenho em notas grandes. + Mover itens marcados + Os itens marcados passam para o final da lista. Desmarque não redefine a posição. + Mostrar alças de movimento + Mostrar a alça para mover itens para cima ou para baixo. Você ainda pode mover as coisas tocando no canto. + Exemplos de remarcação + Configurações de Widget + Alterar as configurações dos widgets da tela principal + Ativar formatação + As notas são renderizadas com formatação nos widgets da tela inicial + Mostrar notas bloqueadas + Permitir que notas bloqueadas sejam exibidas em widgets da tela inicial + Mostrar notas arquivadas + Permitir que anotações arquivadas sejam mostradas em widgets da tela inicial + Mostrar notas no lixo + Permitir que notas na lixeira sejam exibidas em widgets da tela inicial + Ajuda e Perguntas Comuns + Encontre ajuda sobre como usar os recursos no aplicativo + Sincronizando + Backup pendente + Opções de desenvolvedor + Alterar configurações internas para o aplicativo usado para depuração + Exceções de log + Registrar exceções capturadas em uma nota fixa, para permitir o encaminhamento para o desenvolvedor + Mostrar folha de exceção + Mostrar exceções capturadas em uma planilha, se possível, para permitir o encaminhamento para o desenvolvedor + Lançar exceções + Jogue e trave o aplicativo em exceções. Será reposto após 5 falhas + Ativar tela inteira + Torne o aplicativo em tela cheia para permitir capturas de tela e gravações + Mostrar UUIDs de notas + Mostrar os IDs exclusivos das notas na exibição da tela inicial + Exceções falsas + Lance uma exceção falsa para testar os recursos de exceção + Exceção lançada + Bater App + Enviar + Conexão falhada + Sincronizar no Google Drive + O Google Drive permite sincronizar suas notas entre dispositivos usando sua própria conta do Google Drive. + Já fez login no Google Firebase? + Faça login no Google Drive + Assinando em … + Sair do Escarlate + Pare de sincronizar notas, tags e pastas. + Pare de sincronizar suas notas, tags e pastas com seu Google Drive. + Seus dados no aplicativo e no Google Drive ainda estarão lá. + Sair do Drive + Sair … + Notas, etiquetas e pastas são armazenados no seu próprio Google Drive, pelo que apenas pode controlar o acesso a eles. + Suas fotos também são carregadas e sincronizadas em todos os dispositivos. + Restaurar dados do Firebase + Faça login no Google + Assinando em … + Nós armazenamos suas informações no Google Firebase. Após o login, nós os recuperaremos no seu dispositivo + Novas notas e alterações NÃO serão sincronizadas no Google Firebase e você precisará fazer login no Google Drive + Depois que suas anotações forem recuperadas, você poderá excluí-las do Google Firebase e, em seguida, alternar + Seus dados não estão sendo sincronizados! + A Sincronização de dados com base no login herdado está desativada. Considere a possibilidade de alternar para a sincronização baseada no Google Drive. + Transferir dados do Firebase para o Drive + Limpar dados e sair + Limpar e sair + Nós armazenamos suas informações no Google Firebase. Estamos mudando para armazenar suas anotações no seu Google Drive. + Mais privacidade. + Sincronização de fotos. + Próximos passos + Primeiramente, excluiremos suas anotações do Firebase e faremos seu logout. + Você pode fazer login no Google Drive e enviaremos seus dados junto com as imagens. + \ No newline at end of file diff --git a/base/src/main/res/values-ru/strings.xml b/base/src/main/res/values-ru/strings.xml index b86f9709..7695f943 100644 --- a/base/src/main/res/values-ru/strings.xml +++ b/base/src/main/res/values-ru/strings.xml @@ -361,4 +361,98 @@ Последние примечания + + Объединить заметки + Экспортировать как уценку + Экспорт заметок в формате уценки. (Вы не можете импортировать их обратно в приложение) + Папка экспорта + Выберите, где хранится весь экспорт + Используйте цвет темы для фона + Используйте цвет заметки для фона + включить + запрещать + Включить синхронизацию папок + Синхронизация папок + Синхронизируйте все свои заметки, теги и папки с внешней папкой. Это поможет вам синхронизировать использование других приложений между устройствами, а также иметь копию на случай, если вам нужно удалить приложение. + Экспорт в папку + Синхронизировать все заметки, теги и т. Д. Во внешней папке + Экспорт заблокированных заметок + По алфавиту + Доступно на Скарлет Про + Также сделайте резервную копию заметок, которые заблокированы + Параметры редактора + Изменить настройки и использование для редактора заметок + Параметры уценки по умолчанию + Показывать быстрые кнопки уценки на панели инструментов по умолчанию + Уценка в реальном времени + Выберите, если вы хотите, чтобы уценка была видна как ваш тип. Включение может повлиять на производительность на больших заметках. + Переместить отмеченные элементы + Проверенные элементы перемещаются в конец списка. Снять отметку не сбрасывает положение. + Показать ручки движения + Показать ручку для перемещения предметов вверх или вниз. Вы все еще можете перемещать вещи, касаясь угла. + Примеры уценок + Настройки виджета + Изменить настройки виджетов на главном экране + Включить форматирование + Заметки отображаются с форматированием в виджетах главного экрана + Показать заблокированные заметки + Разрешить показ заблокированных заметок в виджетах главного экрана + Показать архивные заметки + Разрешить отображение архивных заметок в виджетах главного экрана + Показать заметки в корзине + Разрешить отображение заметок в корзине на виджетах главного экрана + Помощь и общие вопросы + Найти справку о том, как использовать функции в приложении + Синхронизации + Ожидание резервного копирования + Параметры разработчика + Изменить внутренние настройки для приложения, используемого для отладки + Исключения журнала + Записывать перехваченные исключения в фиксированную заметку, чтобы разрешить пересылку разработчику + Показать лист исключений + По возможности отображать перехваченные исключения на листе, чтобы разрешить пересылку разработчику + Бросить на исключения + Выкинь и вылети приложение на исключения. Будет сброшен после 5 сбоев + Включить полноэкранный режим + Сделайте приложение в полноэкранном режиме, чтобы разрешить скриншоты и записи + Показать примечания UUID + Показать уникальные идентификаторы заметок на главном экране + Поддельные исключения + Создайте ложное исключение, чтобы проверить возможности исключения + Исключение брошено + Crash App + почта + Ошибка подключения + Синхронизация на Google Диске + Google Диск позволяет синхронизировать ваши заметки между устройствами, используя вашу собственную учетную запись Google Drive. + Входили в Google Firebase раньше? + Войдите в Google Drive + Вход в систему… + Выйти из Алого ордена + Прекратите синхронизировать заметки, теги и папки. + Прекратите синхронизировать свои заметки, теги и папки с вашим Google Диском. + Ваши данные в приложении и на Google Диске все еще будут там. + Выйти из Drive + Выход … + Заметки, теги и папки хранятся на вашем собственном Google Диске, поэтому только вы можете контролировать доступ к ним. + Ваши фотографии также загружаются и синхронизируются между устройствами. + Восстановить данные из Firebase + Войти через Google + Вход в систему… + Мы использовали для хранения вашей информации в Google Firebase. После входа мы восстановим их на вашем устройстве. + Новые заметки и изменения НЕ будут синхронизироваться с Google Firebase, и вам необходимо войти на Google Диск + Как только ваши заметки будут восстановлены, вы можете удалить их из Google Firebase, а затем переключиться + Ваши данные не синхронизируются! + Синхронизация данных на основе устаревшего входа отключена, рассмотрите возможность перехода на синхронизацию на Google Диске. + Перенос данных с Firebase на диск + Очистить данные и выйти + Очистка и выход + Мы использовали для хранения вашей информации в Google Firebase. Мы переходим к хранению ваших заметок на вашем Google Диске. + Больше конфиденциальности. + Синхронизация фотографий. + Следующие шаги + Сначала мы удалим ваши заметки из Firebase и выйдем из системы. + Затем вы можете войти в Google Drive, и мы загрузим ваши данные вместе с изображениями. + + diff --git a/base/src/main/res/values-tr/strings.xml b/base/src/main/res/values-tr/strings.xml index f77bd832..d14a25cf 100644 --- a/base/src/main/res/values-tr/strings.xml +++ b/base/src/main/res/values-tr/strings.xml @@ -318,4 +318,97 @@ Uygulamadaki notları, etiketleri ve klasörleri silin Not Görüntüleyici Arka Planı + + Notları Birleştirme + Markdown Olarak Dışa Aktar + Notları işaretleme biçiminde dışa aktarın. (Bunları uygulamaya geri alamazsınız) + Klasörü Ver + Tüm ihracatın nerede saklanacağını seçin + Arka plan için tema rengini kullan + Arka plan için not rengini kullan + etkinleştirme + Devre dışı + Klasör Eşitlemesini Etkinleştir + Folder Sync + Tüm notlarınızı, etiketlerinizi ve klasörlerinizi harici bir klasöre senkronize edin. Bu, diğer uygulamaları kullanarak cihazları senkronize etmenize yardımcı olur, ayrıca uygulamayı silmeniz gerekebilecek bir kopyası da vardır. + Klasöre Verme + Tüm notları, etiketleri vb. Harici bir klasöre senkronize edin. + Kilitli notları dışa aktar + Alfabetik + Scarlet Pro\'da mevcut + Ayrıca kilitlenmiş notları yedekle + Editör Seçenekleri + Not düzenleyicinin ayarlarını ve kullanımını değiştirin + Varsayılan Olarak İşaretleme Seçenekleri + Araç çubuğundaki markdown hızlı düğmelerini varsayılan olarak göster + Gerçek Zamanlı İşaretleme + İşaretlemenin türünüz olarak görünmesini isteyip istemediğinizi seçin. Etkinleştirme, büyük notalarda performansı etkileyebilir. + İşaretli Öğeleri Taşı + Kontrol edilen öğeler listenin en altına taşınır. Seçimi kaldır konumu sıfırlamaz. + Hareket Kollarını Göster + Öğeleri yukarı veya aşağı taşımak için tutamacı gösterin. Köşeye dokunarak şeyleri hareket ettirebilirsiniz. + Markdown Örnekleri + Widget Ayarları + Ana ekran widget\'larının ayarlarını değiştirin + Biçimlendirmeyi Etkinleştir + Notlar, ana ekran widget\'larında biçimlendirme ile işlenir + Kilitli Notları Göster + Kilitli notların ana ekran widget\'larında gösterilmesine izin ver + Arşivlenmiş Notları Göster + Arşivlenmiş notların ana ekran widget\'larında gösterilmesine izin ver + Notları Çöp Kutusunda Göster + Çöp kutusundaki notların ana ekran widget\'larında gösterilmesine izin ver + Yardım ve Sıkça Sorulan Sorular + Uygulamadaki özellikleri nasıl kullanacağınız konusunda yardım bulun + Senkronizasyon + Bekleyen Yedekleme + Geliştirici Seçenekleri + Dahili ayarları hata ayıklama için kullanılan uygulamaya değiştirin + Günlük İstisnaları + Geliştirici\'ye iletime izin vermek için, özel durumları sabit bir nota yakalanan özel durumlar + İstisna Sayfasını Göster + Geliştirici\'ye iletime izin vermek için mümkünse bir sayfada yakalanan istisnaları gösterin + İstisnaları At + İstisnalar üzerine uygulamayı at ve çök. 5 çökmeden sonra sıfırlanacak + Tam Ekranı Etkinleştir + Ekran görüntülerine ve kayıtlara izin vermek için uygulamayı tam ekran yapın + Not UUID\'lerini Göster + Notların benzersiz kimliklerini giriş ekranı görünümünde gösterme + Sahte İstisnalar + İstisna özelliklerini test etmek için sahte bir istisna atın + İstisna Fırlatıldı + Crash Uygulaması + Posta + Bağlantı Başarısız + Google Drive\'da senkronize et + Google Drive, notlarınızı kendi Google Drive hesabınızı kullanarak cihazlar arasında senkronize etmenize olanak sağlar. + Daha önce Google Firebase’e giriş yaptınız mı? + Google Drive\'da Oturum Açın + Oturum Açma… + Scarlet Çıkış + Notları, etiketleri ve klasörleri senkronize etmeyi durdurun. + Notlarınızı, etiketlerinizi ve klasörlerinizi Google Drive\'ınızla senkronize etmeyi bırakın. + Uygulamadaki ve Google Drive\'daki verileriniz hala orada olacak. + Drive\'dan Çıkış Yapın + Çıkış Yapılıyor + Notlar, Etiketler ve Klasörler kendi Google Drive\'ınızda saklanır, böylece bunlara yalnızca siz erişebilirsiniz. + Fotoğraflarınız cihazlar arasında da yüklenir ve senkronize edilir. + Firebase\'den Veri Geri Yükleme + Google ile giriş yap + Oturum Açma… + Bilgilerinizi Google Firebase’de depolardık. Giriş yaptıktan sonra bunları cihazınıza geri yükleyeceğiz. + Yeni notlar ve değişiklikler Google Firebase ile senkronize edilmeyecek ve Google Drive\'da oturum açmanız gerekecek + Notlarınız kurtarıldıktan sonra, Google Firebase\'den silebilir ve ardından geçiş yapabilirsiniz. + Verileriniz senkronize edilmiyor! + Eski oturuma dayalı Veri Senkronizasyonu devre dışı bırakıldı, Google Drive tabanlı senkronizasyona geçmeyi düşünün. + Firebase\'den Drive\'a Veri Aktarma + Verileri Temizle ve Çıkış Yap + Temizleme ve Çıkış Yapma + Bilgilerinizi Google Firebase’de depolardık. Notlarınızı Google Drive\'ınıza kaydetmeye geçiyoruz. + Daha fazla gizlilik. + Fotoğraf Senk. + Sonraki adımlar + İlk önce notlarınızı Firebase\'den silip oturumunuzu kapatırız. + Daha sonra Google Drive’a giriş yapabilirsiniz; verilerinizi resimlerle birlikte yükleriz. + \ No newline at end of file diff --git a/base/src/main/res/values-zh-rCN/strings.xml b/base/src/main/res/values-zh-rCN/strings.xml index a81af2af..98015636 100644 --- a/base/src/main/res/values-zh-rCN/strings.xml +++ b/base/src/main/res/values-zh-rCN/strings.xml @@ -376,5 +376,72 @@ 显示回收站中的笔记 允许回收站中的笔记出现在主屏幕小部件中 - + + + 合并备注 + 出口为降价 + 以降价格式导出备注。 (你不能将这些导回到应用程序) + 导出文件夹 + 选择存储所有导出的位置 + 使用主题颜色为背景 + 使用备注颜色作为背景 + 启用 + 禁用 + 按英文字母顺序 + 可在Scarlet Pro上使用 + 移动选中的项目 + 选中的项目移动到列表的底部。取消选中不会重置位置。 + 帮助和常见问题 + 查找有关如何使用应用程序中的功能的帮助 + 同步 + 待备份 + 开发者选项 + 将内部设置更改为用于调试的应用程序 + 记录例外 + 记录固定注释的异常,以允许转发给Developer + 显示例外表 + 如果可能,在工作表上显示捕获的异常,以允许转发到Developer + 抛弃异常 + 在异常上抛出并崩溃应用程序。 5次崩溃后将重置 + 启用全屏 + 全屏显示应用程序以允许截屏和录制 + 显示注释UUID + 在主屏幕视图中显示备注的唯一ID + 假例外 + 抛出一个假异常来测试异常功能 + 抛出异常 + 崩溃应用程序 + 邮件 + 连接失败 + 在Google云端硬盘上同步 + Google云端硬盘可让您使用自己的Google云端硬盘帐户在设备之间同步笔记。 + 之前登录过Google Firebase? + 登录Google云端硬盘 + 签约 + 退出血色 + 停止同步笔记,标签和文件夹。 + 停止将您的笔记,标签和文件夹同步到Google云端硬盘。 + 您在应用和Google云端硬盘上的数据仍然存在。 + 从云端硬盘退出 + 签出 - + 备注,标签和文件夹存储在您自己的Google云端硬盘中,因此只有您可以控制对它们的访问权限。 + 您的照片也会上传并在设备间同步。 + 从Firebase还原数据 + 与谷歌签约 + 签约 + 我们过去常常将您的信息存储在Google Firebase上。登录后,我们会将这些恢复到您的设备 + 新笔记和更改不会同步到Google Firebase,您需要登录Google云端硬盘 + 恢复笔记后,您可以将其从Google Firebase中删除,然后进行切换 + 您的数据未同步! + 基于旧版登录的数据同步已停用,请考虑切换到基于Google云端硬盘的同步。 + 将数据从Firebase传输到云端硬盘 + 清除数据并注销 + 清除和签出 + 我们过去常常将您的信息存储在Google Firebase上。我们正在将您的笔记存储到您的Google云端硬盘中。 + 更多隐私。 + 照片同步。 + 下一步 + 我们会先从Firebase中删除您的备注并注销。 + 然后,您可以登录Google云端硬盘,我们会将您的数据与图片一起上传。 + diff --git a/base/src/main/res/values-zh-rHK/strings.xml b/base/src/main/res/values-zh-rHK/strings.xml index 3a1b788b..ba1065d2 100644 --- a/base/src/main/res/values-zh-rHK/strings.xml +++ b/base/src/main/res/values-zh-rHK/strings.xml @@ -315,4 +315,97 @@ 刪除應用中的註釋,標籤和文件夾 注意查看器背景 + + 合併備註 + 出口為降價 + 以降價格式導出備註。 (你不能將這些導回到應用程序) + 導出文件夾 + 選擇存儲所有導出的位置 + 使用主題顏色為背景 + 使用備註顏色作為背景 + 啟用 + 禁用 + 啟用文件夾同步 + 文件夾同步 + 將所有筆記,標籤和文件夾同步到外部文件夾。這有助於您在設備之間使用其他應用進行同步,並且可以在您需要刪除應用時獲得副本。 + 導出到文件夾 + 將所有筆記,標籤等同步到外部文件夾 + 導出鎖定的筆記 + 按英文字母順序 + 可在Scarlet Pro上使用 + 還備份已鎖定的備註 + 編輯器選項 + 更改註釋編輯器的設置和用法 + Markdown選項為默認值 + 默認情況下顯示工具欄上的降價快速按鈕 + 實時降價 + 選擇是否要將標記顯示為類型。啟用可以影響大筆記的性能。 + 移動選中的項目 + 選中的項目移動到列表的底部。取消選中不會重置位置。 + 顯示運動手柄 + 顯示向上或向下移動項目的句柄。你仍然可以通過觸摸角落來移動東西。 + 降價示例 + 小部件設置 + 更改主屏幕小組件的設置 + 啟用格式化 + 在主屏幕小部件中使用格式呈現註釋 + 顯示鎖定的筆記 + 允許鎖定的註釋顯示在主屏幕小部件中 + 顯示存檔的筆記 + 允許存檔的註釋顯示在主屏幕小部件中 + 在廢紙簍中顯示備註 + 允許垃圾中的註釋顯示在主屏幕小部件中 + 幫助和常見問題 + 查找有關如何使用應用程序中的功能的幫助 + 同步 + 待備份 + 開發者選項 + 將內部設置更改為用於調試的應用程序 + 記錄例外 + 記錄固定註釋的異常,以允許轉發給Developer + 顯示例外表 + 如果可能,在工作表上顯示捕獲的異常,以允許轉發到Developer + 拋棄異常 + 在異常上拋出並崩潰應用程序。 5次崩潰後將重置 + 啟用全屏 + 全屏顯示應用程序以允許截屏和錄製 + 顯示註釋UUID + 在主屏幕視圖中顯示備註的唯一ID + 假例外 + 拋出一個假異常來測試異常功能 + 拋出異常 + 崩潰應用程序 + 郵件 + 連接失敗 + 在Google雲端硬盤上同步 + Google雲端硬盤可讓您使用自己的Google雲端硬盤帳戶在設備之間同步筆記。 + 之前登錄過Google Firebase? + 登錄Google雲端硬盤 + 簽約 + 退出血色 + 停止同步筆記,標籤和文件夾。 + 停止將您的筆記,標籤和文件夾同步到Google雲端硬盤。 + 您在應用和Google雲端硬盤上的數據仍然存在。 + 從雲端硬盤退出 + 簽出 - + 備註,標籤和文件夾存儲在您自己的Google雲端硬盤中,因此只有您可以控制對它們的訪問權限。 + 您的照片也會上傳並在設備間同步。 + 從Firebase還原數據 + 與穀歌簽約 + 簽約 + 我們過去常常將您的信息存儲在Google Firebase上。登錄後,我們會將這些恢復到您的設備 + 新筆記和更改不會同步到Google Firebase,您需要登錄Google雲端硬盤 + 恢復筆記後,您可以將其從Google Firebase中刪除,然後進行切換 + 您的數據未同步! + 基於舊版登錄的數據同步已停用,請考慮切換到基於Google雲端硬盤的同步。 + 將數據從Firebase傳輸到雲端硬盤 + 清除數據並註銷 + 清除和簽出 + 我們過去常常將您的信息存儲在Google Firebase上。我們正在將您的筆記存儲到您的Google雲端硬盤中。 + 更多隱私。 + 照片同步。 + 下一步 + 我們會先從Firebase中刪除您的備註並註銷。 + 然後,您可以登錄Google雲端硬盤,我們會將您的數據與圖片一起上傳。 + \ No newline at end of file diff --git a/base/src/main/res/values-zh-rMO/strings.xml b/base/src/main/res/values-zh-rMO/strings.xml index 3a1b788b..ba1065d2 100644 --- a/base/src/main/res/values-zh-rMO/strings.xml +++ b/base/src/main/res/values-zh-rMO/strings.xml @@ -315,4 +315,97 @@ 刪除應用中的註釋,標籤和文件夾 注意查看器背景 + + 合併備註 + 出口為降價 + 以降價格式導出備註。 (你不能將這些導回到應用程序) + 導出文件夾 + 選擇存儲所有導出的位置 + 使用主題顏色為背景 + 使用備註顏色作為背景 + 啟用 + 禁用 + 啟用文件夾同步 + 文件夾同步 + 將所有筆記,標籤和文件夾同步到外部文件夾。這有助於您在設備之間使用其他應用進行同步,並且可以在您需要刪除應用時獲得副本。 + 導出到文件夾 + 將所有筆記,標籤等同步到外部文件夾 + 導出鎖定的筆記 + 按英文字母順序 + 可在Scarlet Pro上使用 + 還備份已鎖定的備註 + 編輯器選項 + 更改註釋編輯器的設置和用法 + Markdown選項為默認值 + 默認情況下顯示工具欄上的降價快速按鈕 + 實時降價 + 選擇是否要將標記顯示為類型。啟用可以影響大筆記的性能。 + 移動選中的項目 + 選中的項目移動到列表的底部。取消選中不會重置位置。 + 顯示運動手柄 + 顯示向上或向下移動項目的句柄。你仍然可以通過觸摸角落來移動東西。 + 降價示例 + 小部件設置 + 更改主屏幕小組件的設置 + 啟用格式化 + 在主屏幕小部件中使用格式呈現註釋 + 顯示鎖定的筆記 + 允許鎖定的註釋顯示在主屏幕小部件中 + 顯示存檔的筆記 + 允許存檔的註釋顯示在主屏幕小部件中 + 在廢紙簍中顯示備註 + 允許垃圾中的註釋顯示在主屏幕小部件中 + 幫助和常見問題 + 查找有關如何使用應用程序中的功能的幫助 + 同步 + 待備份 + 開發者選項 + 將內部設置更改為用於調試的應用程序 + 記錄例外 + 記錄固定註釋的異常,以允許轉發給Developer + 顯示例外表 + 如果可能,在工作表上顯示捕獲的異常,以允許轉發到Developer + 拋棄異常 + 在異常上拋出並崩潰應用程序。 5次崩潰後將重置 + 啟用全屏 + 全屏顯示應用程序以允許截屏和錄製 + 顯示註釋UUID + 在主屏幕視圖中顯示備註的唯一ID + 假例外 + 拋出一個假異常來測試異常功能 + 拋出異常 + 崩潰應用程序 + 郵件 + 連接失敗 + 在Google雲端硬盤上同步 + Google雲端硬盤可讓您使用自己的Google雲端硬盤帳戶在設備之間同步筆記。 + 之前登錄過Google Firebase? + 登錄Google雲端硬盤 + 簽約 + 退出血色 + 停止同步筆記,標籤和文件夾。 + 停止將您的筆記,標籤和文件夾同步到Google雲端硬盤。 + 您在應用和Google雲端硬盤上的數據仍然存在。 + 從雲端硬盤退出 + 簽出 - + 備註,標籤和文件夾存儲在您自己的Google雲端硬盤中,因此只有您可以控制對它們的訪問權限。 + 您的照片也會上傳並在設備間同步。 + 從Firebase還原數據 + 與穀歌簽約 + 簽約 + 我們過去常常將您的信息存儲在Google Firebase上。登錄後,我們會將這些恢復到您的設備 + 新筆記和更改不會同步到Google Firebase,您需要登錄Google雲端硬盤 + 恢復筆記後,您可以將其從Google Firebase中刪除,然後進行切換 + 您的數據未同步! + 基於舊版登錄的數據同步已停用,請考慮切換到基於Google雲端硬盤的同步。 + 將數據從Firebase傳輸到雲端硬盤 + 清除數據並註銷 + 清除和簽出 + 我們過去常常將您的信息存儲在Google Firebase上。我們正在將您的筆記存儲到您的Google雲端硬盤中。 + 更多隱私。 + 照片同步。 + 下一步 + 我們會先從Firebase中刪除您的備註並註銷。 + 然後,您可以登錄Google雲端硬盤,我們會將您的數據與圖片一起上傳。 + \ No newline at end of file diff --git a/base/src/main/res/values-zh-rTW/strings.xml b/base/src/main/res/values-zh-rTW/strings.xml index 3a1b788b..ba1065d2 100644 --- a/base/src/main/res/values-zh-rTW/strings.xml +++ b/base/src/main/res/values-zh-rTW/strings.xml @@ -315,4 +315,97 @@ 刪除應用中的註釋,標籤和文件夾 注意查看器背景 + + 合併備註 + 出口為降價 + 以降價格式導出備註。 (你不能將這些導回到應用程序) + 導出文件夾 + 選擇存儲所有導出的位置 + 使用主題顏色為背景 + 使用備註顏色作為背景 + 啟用 + 禁用 + 啟用文件夾同步 + 文件夾同步 + 將所有筆記,標籤和文件夾同步到外部文件夾。這有助於您在設備之間使用其他應用進行同步,並且可以在您需要刪除應用時獲得副本。 + 導出到文件夾 + 將所有筆記,標籤等同步到外部文件夾 + 導出鎖定的筆記 + 按英文字母順序 + 可在Scarlet Pro上使用 + 還備份已鎖定的備註 + 編輯器選項 + 更改註釋編輯器的設置和用法 + Markdown選項為默認值 + 默認情況下顯示工具欄上的降價快速按鈕 + 實時降價 + 選擇是否要將標記顯示為類型。啟用可以影響大筆記的性能。 + 移動選中的項目 + 選中的項目移動到列表的底部。取消選中不會重置位置。 + 顯示運動手柄 + 顯示向上或向下移動項目的句柄。你仍然可以通過觸摸角落來移動東西。 + 降價示例 + 小部件設置 + 更改主屏幕小組件的設置 + 啟用格式化 + 在主屏幕小部件中使用格式呈現註釋 + 顯示鎖定的筆記 + 允許鎖定的註釋顯示在主屏幕小部件中 + 顯示存檔的筆記 + 允許存檔的註釋顯示在主屏幕小部件中 + 在廢紙簍中顯示備註 + 允許垃圾中的註釋顯示在主屏幕小部件中 + 幫助和常見問題 + 查找有關如何使用應用程序中的功能的幫助 + 同步 + 待備份 + 開發者選項 + 將內部設置更改為用於調試的應用程序 + 記錄例外 + 記錄固定註釋的異常,以允許轉發給Developer + 顯示例外表 + 如果可能,在工作表上顯示捕獲的異常,以允許轉發到Developer + 拋棄異常 + 在異常上拋出並崩潰應用程序。 5次崩潰後將重置 + 啟用全屏 + 全屏顯示應用程序以允許截屏和錄製 + 顯示註釋UUID + 在主屏幕視圖中顯示備註的唯一ID + 假例外 + 拋出一個假異常來測試異常功能 + 拋出異常 + 崩潰應用程序 + 郵件 + 連接失敗 + 在Google雲端硬盤上同步 + Google雲端硬盤可讓您使用自己的Google雲端硬盤帳戶在設備之間同步筆記。 + 之前登錄過Google Firebase? + 登錄Google雲端硬盤 + 簽約 + 退出血色 + 停止同步筆記,標籤和文件夾。 + 停止將您的筆記,標籤和文件夾同步到Google雲端硬盤。 + 您在應用和Google雲端硬盤上的數據仍然存在。 + 從雲端硬盤退出 + 簽出 - + 備註,標籤和文件夾存儲在您自己的Google雲端硬盤中,因此只有您可以控制對它們的訪問權限。 + 您的照片也會上傳並在設備間同步。 + 從Firebase還原數據 + 與穀歌簽約 + 簽約 + 我們過去常常將您的信息存儲在Google Firebase上。登錄後,我們會將這些恢復到您的設備 + 新筆記和更改不會同步到Google Firebase,您需要登錄Google雲端硬盤 + 恢復筆記後,您可以將其從Google Firebase中刪除,然後進行切換 + 您的數據未同步! + 基於舊版登錄的數據同步已停用,請考慮切換到基於Google雲端硬盤的同步。 + 將數據從Firebase傳輸到雲端硬盤 + 清除數據並註銷 + 清除和簽出 + 我們過去常常將您的信息存儲在Google Firebase上。我們正在將您的筆記存儲到您的Google雲端硬盤中。 + 更多隱私。 + 照片同步。 + 下一步 + 我們會先從Firebase中刪除您的備註並註銷。 + 然後,您可以登錄Google雲端硬盤,我們會將您的數據與圖片一起上傳。 + \ No newline at end of file diff --git a/base/src/main/res/values-zh/strings.xml b/base/src/main/res/values-zh/strings.xml index 3a1b788b..ba1065d2 100644 --- a/base/src/main/res/values-zh/strings.xml +++ b/base/src/main/res/values-zh/strings.xml @@ -315,4 +315,97 @@ 刪除應用中的註釋,標籤和文件夾 注意查看器背景 + + 合併備註 + 出口為降價 + 以降價格式導出備註。 (你不能將這些導回到應用程序) + 導出文件夾 + 選擇存儲所有導出的位置 + 使用主題顏色為背景 + 使用備註顏色作為背景 + 啟用 + 禁用 + 啟用文件夾同步 + 文件夾同步 + 將所有筆記,標籤和文件夾同步到外部文件夾。這有助於您在設備之間使用其他應用進行同步,並且可以在您需要刪除應用時獲得副本。 + 導出到文件夾 + 將所有筆記,標籤等同步到外部文件夾 + 導出鎖定的筆記 + 按英文字母順序 + 可在Scarlet Pro上使用 + 還備份已鎖定的備註 + 編輯器選項 + 更改註釋編輯器的設置和用法 + Markdown選項為默認值 + 默認情況下顯示工具欄上的降價快速按鈕 + 實時降價 + 選擇是否要將標記顯示為類型。啟用可以影響大筆記的性能。 + 移動選中的項目 + 選中的項目移動到列表的底部。取消選中不會重置位置。 + 顯示運動手柄 + 顯示向上或向下移動項目的句柄。你仍然可以通過觸摸角落來移動東西。 + 降價示例 + 小部件設置 + 更改主屏幕小組件的設置 + 啟用格式化 + 在主屏幕小部件中使用格式呈現註釋 + 顯示鎖定的筆記 + 允許鎖定的註釋顯示在主屏幕小部件中 + 顯示存檔的筆記 + 允許存檔的註釋顯示在主屏幕小部件中 + 在廢紙簍中顯示備註 + 允許垃圾中的註釋顯示在主屏幕小部件中 + 幫助和常見問題 + 查找有關如何使用應用程序中的功能的幫助 + 同步 + 待備份 + 開發者選項 + 將內部設置更改為用於調試的應用程序 + 記錄例外 + 記錄固定註釋的異常,以允許轉發給Developer + 顯示例外表 + 如果可能,在工作表上顯示捕獲的異常,以允許轉發到Developer + 拋棄異常 + 在異常上拋出並崩潰應用程序。 5次崩潰後將重置 + 啟用全屏 + 全屏顯示應用程序以允許截屏和錄製 + 顯示註釋UUID + 在主屏幕視圖中顯示備註的唯一ID + 假例外 + 拋出一個假異常來測試異常功能 + 拋出異常 + 崩潰應用程序 + 郵件 + 連接失敗 + 在Google雲端硬盤上同步 + Google雲端硬盤可讓您使用自己的Google雲端硬盤帳戶在設備之間同步筆記。 + 之前登錄過Google Firebase? + 登錄Google雲端硬盤 + 簽約 + 退出血色 + 停止同步筆記,標籤和文件夾。 + 停止將您的筆記,標籤和文件夾同步到Google雲端硬盤。 + 您在應用和Google雲端硬盤上的數據仍然存在。 + 從雲端硬盤退出 + 簽出 - + 備註,標籤和文件夾存儲在您自己的Google雲端硬盤中,因此只有您可以控制對它們的訪問權限。 + 您的照片也會上傳並在設備間同步。 + 從Firebase還原數據 + 與穀歌簽約 + 簽約 + 我們過去常常將您的信息存儲在Google Firebase上。登錄後,我們會將這些恢復到您的設備 + 新筆記和更改不會同步到Google Firebase,您需要登錄Google雲端硬盤 + 恢復筆記後,您可以將其從Google Firebase中刪除,然後進行切換 + 您的數據未同步! + 基於舊版登錄的數據同步已停用,請考慮切換到基於Google雲端硬盤的同步。 + 將數據從Firebase傳輸到雲端硬盤 + 清除數據並註銷 + 清除和簽出 + 我們過去常常將您的信息存儲在Google Firebase上。我們正在將您的筆記存儲到您的Google雲端硬盤中。 + 更多隱私。 + 照片同步。 + 下一步 + 我們會先從Firebase中刪除您的備註並註銷。 + 然後,您可以登錄Google雲端硬盤,我們會將您的數據與圖片一起上傳。 + \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index dc0b7302..d87de7ca 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -43,7 +43,7 @@ Pin Note Unpin Note Distraction Free - Merge Notes + Merge Notes Choose Action… Tap to view the note in night mode @@ -126,10 +126,10 @@ Export notes to device storage to share Import Notes Import notes from the device storage - Export As Markdown - Export notes in markdown format. (You cannot import these back to the app) - Export Folder - Choose where all exports are stored + Export As Markdown + Export notes in markdown format. (You cannot import these back to the app) + Export Folder + Choose where all exports are stored Export Automatically Export notes frequently to external file as backup About Us @@ -156,8 +156,8 @@ Delete notes, tags and folders in the app Note Viewer Background - Use theme color for background - Use note color for background + Use theme color for background + Use note color for background Security Options Pass Code @@ -199,16 +199,16 @@ Exporting to file… Done Share - Enable - Disable - Enable Folder Sync - Folder Sync - Sync all your notes, tags and folders to an external folder. This helps you sync using other apps between devices, as well have a copy in case you need to delete the app. - Exporting to Folder - Sync all the notes, tags, etc to an external folder + Enable + Disable + Enable Folder Sync + Folder Sync + Sync all your notes, tags and folders to an external folder. This helps you sync using other apps between devices, as well have a copy in case you need to delete the app. + Exporting to Folder + Sync all the notes, tags, etc to an external folder MaterialNotes/BACKUP.txt - Export locked notes - Also backup the notes which are locked + Export locked notes + Also backup the notes which are locked %s KB %s MB @@ -256,7 +256,7 @@ Newest First Oldest First Most Recently Modified - Alphabetical + Alphabetical Select a Note @@ -337,7 +337,7 @@ App privacy policy for the content Install Pro App - Available on Scarlet Pro + Available on Scarlet Pro Why Install Pro @@ -392,110 +392,110 @@ - Editor Options - Change the settings and usage for the note editor + Editor Options + Change the settings and usage for the note editor - Markdown Options as Default - Show the markdown quick buttons on the toolbar as default + Markdown Options as Default + Show the markdown quick buttons on the toolbar as default - Realtime Markdown - Choose if you want markdown to be visible as your type. Enabling can effect performance on large notes. + Realtime Markdown + Choose if you want markdown to be visible as your type. Enabling can effect performance on large notes. - Move Checked Items - Checked items move to the bottom of the list. Uncheck does not reset position. + Move Checked Items + Checked items move to the bottom of the list. Uncheck does not reset position. - Show Movement Handles - Show the handle to move items up or down. You can still move things around by touching the corner. + Show Movement Handles + Show the handle to move items up or down. You can still move things around by touching the corner. - Markdown Examples + Markdown Examples - Widget Settings - Change the settings for the home screen widgets + Widget Settings + Change the settings for the home screen widgets - Enable Formatting - Notes are rendered with formatting in home screen widgets + Enable Formatting + Notes are rendered with formatting in home screen widgets - Show Locked Notes - Allow locked notes to be shown in home screen widgets + Show Locked Notes + Allow locked notes to be shown in home screen widgets - Show Archived Notes - Allow archived notes to be shown in home screen widgets + Show Archived Notes + Allow archived notes to be shown in home screen widgets - Show Notes in Trash - Allow notes in trash to be shown in home screen widgets + Show Notes in Trash + Allow notes in trash to be shown in home screen widgets - Help and Common Questions - Find help on how to use the features in the app + Help and Common Questions + Find help on how to use the features in the app - Syncing - Pending Backup + Syncing + Pending Backup - Developer Options - Change internal settings to the application used for debugging + Developer Options + Change internal settings to the application used for debugging - Log Exceptions - Log caught exceptions to a fixed note, to allow forwarding to Developer + Log Exceptions + Log caught exceptions to a fixed note, to allow forwarding to Developer - Show Exception Sheet - Show caught exceptions on a sheet if possible, to allow forwarding to Developer + Show Exception Sheet + Show caught exceptions on a sheet if possible, to allow forwarding to Developer - Throw on Exceptions - Throw and crash the application on exceptions. Will be reset after 5 crashes + Throw on Exceptions + Throw and crash the application on exceptions. Will be reset after 5 crashes - Enable Fullscreen - Make the app in fullscreen to allow screenshots and recordings + Enable Fullscreen + Make the app in fullscreen to allow screenshots and recordings - Show Note UUIDs - Show the unique ids of the notes in the home screen view + Show Note UUIDs + Show the unique ids of the notes in the home screen view - Fake Exceptions - Throw a fake exception to test the exception features + Fake Exceptions + Throw a fake exception to test the exception features - Exception Thrown - Crash App - Mail + Exception Thrown + Crash App + Mail - Connection Failed - Sync on Google Drive - Google Drive allows you to sync your notes between devices using your own Google Drive account. - Logged into Google Firebase before? - Sign In to Google Drive - Signing In… - - Sign Out of Scarlet - Stop syncing notes, tags and folders. - Stop syncing your notes, tags and folders to your Google Drive. - Your data on the app and on Google Drive will still be there. - Sign Out from Drive - Signing Out… - - Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. - Your photos are uploaded and synced across devices as well. + Connection Failed + Sync on Google Drive + Google Drive allows you to sync your notes between devices using your own Google Drive account. + Logged into Google Firebase before? + Sign In to Google Drive + Signing In… + + Sign Out of Scarlet + Stop syncing notes, tags and folders. + Stop syncing your notes, tags and folders to your Google Drive. + Your data on the app and on Google Drive will still be there. + Sign Out from Drive + Signing Out… + + Notes, Tags and Folders are stored on your own Google Drive, so only you can control access to them. + Your photos are uploaded and synced across devices as well. - Restore Data from Firebase - Sign In with Google - Signing In… - - We used to store your information on Google Firebase. After logging in we will recover these to your device - New notes and changes will NOT be synced over to Google Firebase, and you need to login on to Google Drive - Once your notes are recovered, you can delete them from Google Firebase, and then switch over - - Your data is not being synced! - Data Sync based on legacy login is disabled, consider switching over to Google Drive based sync. - - Transfer Data from Firebase to Drive - Clear Data and Sign Out - Clearing and Signing Out - We used to store your information on Google Firebase. We are moving over to storing your notes in your Google Drive. - More Privacy. - Photo Sync. - Next Steps - We will first delete your notes from Firebase and log you out. - You can then log into Google Drive, and we will upload your data along with images. + Restore Data from Firebase + Sign In with Google + Signing In… + + We used to store your information on Google Firebase. After logging in we will recover these to your device + New notes and changes will NOT be synced over to Google Firebase, and you need to login on to Google Drive + Once your notes are recovered, you can delete them from Google Firebase, and then switch over + + Your data is not being synced! + Data Sync based on legacy login is disabled, consider switching over to Google Drive based sync. + + Transfer Data from Firebase to Drive + Clear Data and Sign Out + Clearing and Signing Out + We used to store your information on Google Firebase. We are moving over to storing your notes in your Google Drive. + More Privacy. + Photo Sync. + Next Steps + We will first delete your notes from Firebase and log you out. + You can then log into Google Drive, and we will upload your data along with images. From a2390ae22c9609761d7138b0b1118d6bc0683d55 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 29 Jul 2019 20:30:16 +0100 Subject: [PATCH 047/134] [AppLock] Introducing App Lock --- base/src/main/AndroidManifest.xml | 12 +- .../com/maubis/scarlet/base/MainActivity.kt | 3 +- .../export/activity/ImportNoteActivity.kt | 3 +- .../sheet/BackupSettingsOptionsBottomSheet.kt | 2 +- .../activity/OpenTextIntentOrFileActivity.kt | 3 +- .../scarlet/base/note/NoteExtensions.kt | 2 +- .../note/actions/NoteOptionsBottomSheet.kt | 2 +- .../creation/activity/ViewNoteActivity.kt | 2 +- .../base/note/recycler/NoteRecyclerHolder.kt | 2 +- .../activity/SelectableNotesActivityBase.kt | 4 +- .../sheet/SelectedNoteOptionsBottomSheet.kt | 2 +- .../base/security/activity/AppLockActivity.kt | 76 ++++++++++++ .../security/activity/AppLockActivitySpecs.kt | 113 ++++++++++++++++++ .../security/controller/AppLockController.kt | 31 +++++ .../sheets/EnterPincodeBottomSheet.kt | 14 +-- .../sheets/NoPincodeBottomSheet.kt | 2 +- .../sheet/SecurityOptionsBottomSheet.kt | 95 +++++++++------ .../base/support/ui/SecuredActivity.kt | 14 +++ base/src/main/res/values/strings.xml | 10 ++ .../quicknote/drive/GDriveLoginActivity.kt | 3 +- 20 files changed, 340 insertions(+), 55 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt rename base/src/main/java/com/maubis/scarlet/base/{main => security}/sheets/EnterPincodeBottomSheet.kt (93%) rename base/src/main/java/com/maubis/scarlet/base/{main => security}/sheets/NoPincodeBottomSheet.kt (98%) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index d5a34183..a280d8f8 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -1,2 +1,12 @@ + + package="com.maubis.scarlet.base"> + + + + + + \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 0e085157..587cc7f3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -52,6 +52,7 @@ import com.maubis.scarlet.base.support.database.Migrator import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.ToolbarColorConfig +import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.unifiedFolderSearchSynchronous @@ -63,7 +64,7 @@ import kotlinx.android.synthetic.main.toolbar_trash_info.* import kotlinx.coroutines.* import java.util.concurrent.atomic.AtomicBoolean -class MainActivity : ThemedActivity(), INoteOptionSheetActivity { +class MainActivity : SecuredActivity(), INoteOptionSheetActivity { private val singleThreadDispatcher = newSingleThreadContext("singleThreadDispatcher") private lateinit var recyclerView: RecyclerView diff --git a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt index 06030bd9..6195019c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt @@ -14,6 +14,7 @@ import com.maubis.scarlet.base.export.recycler.FileRecyclerItem import com.maubis.scarlet.base.export.support.NoteImporter import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.support.recycler.RecyclerItem +import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.bind @@ -21,7 +22,7 @@ import java.io.File import java.io.FileReader -class ImportNoteActivity : ThemedActivity() { +class ImportNoteActivity : SecuredActivity() { val adapter = NoteAppAdapter(this) var currentlySelectedFile: File? = null diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt index 422b4121..f9bb9e70 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt @@ -8,7 +8,7 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.export.support.PermissionUtils -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 99e9d825..685d2088 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -23,6 +23,7 @@ import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.support.NoteImporter import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.save +import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.bind @@ -32,7 +33,7 @@ import java.io.InputStreamReader const val KEEP_PACKAGE = "com.google.android.keep" const val INTENT_KEY_DIRECT_NOTES_TRANSFER = "direct_notes_transfer" -class OpenTextIntentOrFileActivity : ThemedActivity() { +class OpenTextIntentOrFileActivity : SecuredActivity() { lateinit var context: Context diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index f4987724..8679f507 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -16,7 +16,7 @@ import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.* import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_DISTRACTION_FREE import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 156020f5..0cf870ac 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -14,7 +14,7 @@ import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getNoteState import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteNotePermanentlySheet import com.maubis.scarlet.base.note.* diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 01fb1abf..1a2e6894 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -57,7 +57,7 @@ data class NoteViewColorConfig( var toolbarIconColor: Int = Color.BLACK, var statusBarColor: Int = Color.BLACK) -open class ViewAdvancedNoteActivity : ThemedActivity(), INoteOptionSheetActivity, IFormatRecyclerViewActivity { +open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivity, IFormatRecyclerViewActivity { var focusedFormat: Format? = null diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt index 862035e6..23236da4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt @@ -5,7 +5,7 @@ import android.os.Bundle import android.view.View import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.note.actions.NoteOptionsBottomSheet import com.maubis.scarlet.base.note.copy import com.maubis.scarlet.base.note.edit diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index 39bfc30f..e7e5c6af 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -19,10 +19,10 @@ import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.note.recycler.NoteRecyclerItem import com.maubis.scarlet.base.note.recycler.getSelectableRecyclerItemControllerList import com.maubis.scarlet.base.settings.sheet.* +import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity -abstract class SelectableNotesActivityBase : ThemedActivity(), INoteSelectorActivity { +abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorActivity { lateinit var recyclerView: RecyclerView lateinit var adapter: NoteAppAdapter diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt index d7bd15fd..66ceb414 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt @@ -9,7 +9,7 @@ import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.sectionPreservingSort import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getFormats -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.folder.sheet.SelectedFolderChooseOptionsBottomSheet import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt new file mode 100644 index 00000000..3e379bc8 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt @@ -0,0 +1,76 @@ +package com.maubis.scarlet.base.security.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.LithoView +import com.github.ajalt.reprint.core.AuthenticationFailureReason +import com.github.ajalt.reprint.core.AuthenticationListener +import com.github.ajalt.reprint.core.Reprint +import com.maubis.scarlet.base.security.controller.AppLockController +import com.maubis.scarlet.base.settings.sheet.sSecurityCode +import com.maubis.scarlet.base.settings.sheet.sSecurityFingerprintEnabled +import com.maubis.scarlet.base.support.ui.ThemedActivity + +class AppLockActivity : ThemedActivity() { + lateinit var context: Context + lateinit var component: Component + lateinit var componentContext: ComponentContext + + private var passCodeEntered: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + context = this + componentContext = ComponentContext(context) + + setView() + notifyThemeChange() + } + private fun setView() { + component = AppLockView.create(componentContext) + .fingerprintEnabled(Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled) + .onTextChange { text -> + passCodeEntered = text + } + .onClick { + if (passCodeEntered.length == 4 && sSecurityCode == passCodeEntered) { + AppLockController.notifyAppLock() + finish() + } + } + .build() + setContentView(LithoView.create(componentContext, component)) + } + + override fun onResume() { + super.onResume() + passCodeEntered = "" + Reprint.authenticate(object : AuthenticationListener { + override fun onSuccess(moduleTag: Int) { + AppLockController.notifyAppLock() + finish() + } + + override fun onFailure( + failureReason: AuthenticationFailureReason?, + fatal: Boolean, + errorMessage: CharSequence?, + moduleTag: Int, + errorCode: Int) { + // Ignore + } + }) + } + override fun onPause() { + super.onPause() + Reprint.cancelAuthentication() + } + + override fun notifyThemeChange() { + setSystemTheme() + } +} diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt new file mode 100644 index 00000000..92d4c9cc --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -0,0 +1,113 @@ +package com.maubis.scarlet.base.security.activity + +import android.graphics.Color +import android.text.InputType +import android.text.Layout +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.EditText +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.TextChangedEvent +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.support.specs.EmptySpec +import com.maubis.scarlet.base.support.ui.ThemeColorType + +@LayoutSpec +object AppLockViewSpec { + + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop fingerprintEnabled: Boolean, + @Prop onTextChange: (String) -> Unit): Component { + return Column.create(context) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(AppLockContentView.create(context) + .fingerprintEnabled(fingerprintEnabled) + .onTextChange(onTextChange) + .flexGrow(1f)) + .child(Row.create(context) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + when { + fingerprintEnabled -> Image.create(context) + .drawableRes(R.drawable.ic_option_fingerprint) + .heightDip(36f) + else -> null + } + ) + .child(EmptySpec.create(context).flexGrow(1f)) + .child(Text.create(context) + .backgroundRes(R.drawable.accent_rounded_bg) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(R.string.security_sheet_button_unlock) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .typeface(CoreConfig.FONT_MONSERRAT) + .clickHandler(AppLockView.onUnlockClick(context)))) + .build() + } + + @OnEvent(ClickEvent::class) + fun onUnlockClick(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + +@LayoutSpec +object AppLockContentViewSpec { + + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop fingerprintEnabled: Boolean): Component { + val description = when { + fingerprintEnabled -> R.string.app_lock_details + else -> R.string.app_lock_details_no_fingerprint + } + return Column.create(context) + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.app_lock_title) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textRes(description) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(EmptySpec.create(context).flexGrow(1f)) + .child(EditText.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .textSizeRes(R.dimen.font_size_xlarge) + .minWidthDip(128f) + .maxLength(4) + .alignSelf(YogaAlign.CENTER) + .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_OPEN_SANS) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .paddingDip(YogaEdge.HORIZONTAL, 22f) + .paddingDip(YogaEdge.VERTICAL, 6f) + + .textChangedEventHandler(AppLockContentView.onTextChanged(context))) + .child(EmptySpec.create(context).flexGrow(1f)) + .build() + } + + @OnEvent(TextChangedEvent::class) + fun onTextChanged(context: ComponentContext, @FromEvent text: String, @Prop onTextChange: (String) -> Unit) { + onTextChange(text) + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt b/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt new file mode 100644 index 00000000..7f6359d4 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt @@ -0,0 +1,31 @@ +package com.maubis.scarlet.base.security.controller + +import android.os.SystemClock +import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled +import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled + +object AppLockController { + private var sLastLoginTimeMs = 0L + + fun needsAppLock(): Boolean { + if (hasPinCodeEnabled() && sSecurityAppLockEnabled) { + // App lock enabled + val deltaSinceLastUnlock = SystemClock.uptimeMillis() - sLastLoginTimeMs + + // unlock stays 10 minutes + if (deltaSinceLastUnlock > 1000 * 60 * 10) { + return true + } + + // reset lock time + notifyAppLock() + return false + } + return false + } + + fun notifyAppLock() { + sLastLoginTimeMs = SystemClock.uptimeMillis() + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt similarity index 93% rename from base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt rename to base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt index a12a75fe..415ce1ad 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/EnterPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt @@ -1,4 +1,4 @@ -package com.maubis.scarlet.base.main.sheets +package com.maubis.scarlet.base.security.sheets import android.app.Dialog import android.content.DialogInterface @@ -17,8 +17,9 @@ import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled +import com.maubis.scarlet.base.settings.sheet.sSecurityCode +import com.maubis.scarlet.base.settings.sheet.sSecurityFingerprintEnabled import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -176,7 +177,7 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { override fun isRemoveButtonEnabled(): Boolean = true override fun onRemoveButtonClick() { - ApplicationBase.instance.store().put(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, "") + sSecurityCode = "" sNoPinSetupNoticeShown = false listener.onSuccess() @@ -185,7 +186,7 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { } override fun onPasswordRequested(password: String) { - ApplicationBase.instance.store().put(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, password) + sSecurityCode = password listener.onSuccess() } @@ -238,12 +239,11 @@ class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { override fun getActionTitle(): Int = actionTitle override fun isFingerprintEnabled(): Boolean { - return Reprint.hasFingerprintRegistered() && - ApplicationBase.instance.store().get(SecurityOptionsBottomSheet.KEY_FINGERPRINT_ENABLED, true) + return Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled } override fun onPasswordRequested(password: String) { - val currentPassword = ApplicationBase.instance.store().get(SecurityOptionsBottomSheet.KEY_SECURITY_CODE, "") + val currentPassword = sSecurityCode if (currentPassword != "" && currentPassword == password) { listener.onSuccess() } else if (listener is PincodeSuccessListener) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt similarity index 98% rename from base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt rename to base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt index 6ba53b23..7f13b9eb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt @@ -1,4 +1,4 @@ -package com.maubis.scarlet.base.main.sheets +package com.maubis.scarlet.base.security.sheets import android.app.Dialog import com.facebook.litho.Column diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index 9a4f1cb7..551fd9f1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -7,58 +7,91 @@ import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet.Companion.openCreateSheet -import com.maubis.scarlet.base.main.sheets.EnterPincodeBottomSheet.Companion.openVerifySheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet.Companion.openCreateSheet +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet.Companion.openVerifySheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.ui.ThemedActivity +const val KEY_SECURITY_CODE = "KEY_SECURITY_CODE" +const val KEY_FINGERPRINT_ENABLED = "KEY_FINGERPRINT_ENABLED" +const val KEY_APP_LOCK_ENABLED = "app_lock_enabled" + +var sSecurityCode: String + get() = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") + set(value) = ApplicationBase.instance.store().put(KEY_SECURITY_CODE, value) +var sSecurityFingerprintEnabled: Boolean + get() = ApplicationBase.instance.store().get(KEY_FINGERPRINT_ENABLED, true) + set(value) = ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, value) +var sSecurityAppLockEnabled: Boolean + get() = ApplicationBase.instance.store().get(KEY_APP_LOCK_ENABLED, false) + set(value) = ApplicationBase.instance.store().put(KEY_APP_LOCK_ENABLED, value) + class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.security_option_title override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val options = ArrayList() + options.add(LithoOptionsItem( title = R.string.security_option_set_pin_code, subtitle = R.string.security_option_set_pin_code_subtitle, icon = R.drawable.ic_option_security, listener = { - val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") - val hasPinCode = !TextUtils.isNullOrEmpty(currentPinCode) - if (hasPinCode) { - openResetPasswordDialog(dialog) - } else { - openCreatePasswordDialog(dialog) + when { + hasPinCodeEnabled() -> openResetPasswordDialog(dialog) + else -> openCreatePasswordDialog(dialog) + } + }, + isSelectable = true, + selected = hasPinCodeEnabled() + )) + + options.add(LithoOptionsItem( + title = R.string.security_option_lock_app, + subtitle = R.string.security_option_lock_app_details, + icon = R.drawable.ic_option_security, + listener = { + when { + hasPinCodeEnabled() -> openVerifyPasswordDialog( + object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { + override fun onSuccess() { + sSecurityAppLockEnabled = !sSecurityAppLockEnabled + reset(componentContext.androidContext, dialog) + } + } + ) + else -> openCreatePasswordDialog(dialog) } }, isSelectable = true, - selected = !TextUtils.isNullOrEmpty(ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "")) + selected = sSecurityAppLockEnabled )) + val hasFingerprint = Reprint.hasFingerprintRegistered() options.add(LithoOptionsItem( title = R.string.security_option_fingerprint_enabled, subtitle = R.string.security_option_fingerprint_enabled_subtitle, icon = R.drawable.ic_option_fingerprint, listener = { - val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") - val hasPinCode = !TextUtils.isNullOrEmpty(currentPinCode) - if (hasPinCode) { - openVerifyPasswordDialog( + when { + hasPinCodeEnabled() -> openVerifyPasswordDialog( object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { override fun onSuccess() { - ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, false) + sSecurityFingerprintEnabled = false reset(componentContext.androidContext, dialog) } } ) - } else { - ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, false) - reset(componentContext.androidContext, dialog) + else -> { + sSecurityFingerprintEnabled = false + reset(componentContext.androidContext, dialog) + } } }, - visible = ApplicationBase.instance.store().get(KEY_FINGERPRINT_ENABLED, true) && hasFingerprint, + visible = sSecurityFingerprintEnabled && hasFingerprint, isSelectable = true, selected = true )) @@ -67,23 +100,22 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.security_option_fingerprint_disabled_subtitle, icon = R.drawable.ic_option_fingerprint, listener = { - val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") - val hasPinCode = !TextUtils.isNullOrEmpty(currentPinCode) - if (hasPinCode) { - openVerifyPasswordDialog( + when { + hasPinCodeEnabled() -> openVerifyPasswordDialog( object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { override fun onSuccess() { - ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, true) + sSecurityFingerprintEnabled = true reset(componentContext.androidContext, dialog) } } ) - } else { - ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, true) - reset(componentContext.androidContext, dialog) + else -> { + sSecurityFingerprintEnabled = true + reset(componentContext.androidContext, dialog) + } } }, - visible = !ApplicationBase.instance.store().get(KEY_FINGERPRINT_ENABLED, true) && hasFingerprint + visible = !sSecurityFingerprintEnabled && hasFingerprint )) return options } @@ -130,18 +162,13 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { } companion object { - - const val KEY_SECURITY_CODE = "KEY_SECURITY_CODE" - const val KEY_FINGERPRINT_ENABLED = "KEY_FINGERPRINT_ENABLED" - fun openSheet(activity: MainActivity) { val sheet = SecurityOptionsBottomSheet() sheet.show(activity.supportFragmentManager, sheet.tag) } fun hasPinCodeEnabled(): Boolean { - val currentPinCode = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") - return !TextUtils.isNullOrEmpty(currentPinCode) + return !TextUtils.isNullOrEmpty(sSecurityCode) } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt new file mode 100644 index 00000000..214f0c77 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt @@ -0,0 +1,14 @@ +package com.maubis.scarlet.base.support.ui + +import android.content.Intent +import com.maubis.scarlet.base.security.activity.AppLockActivity +import com.maubis.scarlet.base.security.controller.AppLockController + +abstract class SecuredActivity : ThemedActivity() { + override fun onResume() { + super.onResume() + if (AppLockController.needsAppLock()) { + startActivity(Intent(this, AppLockActivity::class.java)) + } + } +} \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index d87de7ca..4ad3656a 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -162,6 +162,8 @@ Security Options Pass Code Set a 4 digit PIN to lock notes + Enable App Lock + Use PIN to unlock the whole application Unlock with Fingerprint The notes will unlock with the fingerprint Fingerprint disabled @@ -498,4 +500,12 @@ Next Steps We will first delete your notes from Firebase and log you out. You can then log into Google Drive, and we will upload your data along with images. + + + + Unlock App + Enter PIN or use fingerprint to unlock + Enter PIN to unlock + + diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 078e83fe..c9351294 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -29,6 +29,7 @@ import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers @@ -42,7 +43,7 @@ import java.util.concurrent.atomic.AtomicBoolean // TODO: This is not ready... Recent changes in Drive API make this sh*t a little difficult and // inconclusive. I want to do this because it's safer than Firebase, but f*ck Google for // changing the API So much -class GDriveLoginActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { +class GDriveLoginActivity : SecuredActivity(), GoogleApiClient.OnConnectionFailedListener { private val RC_SIGN_IN = 31244 From 1fe86f16d0e87403228e0abb385fbce77e3d135e Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 29 Jul 2019 21:03:38 +0100 Subject: [PATCH 048/134] Fixing Markdown in Code, Pin Title --- .../scarlet/base/note/NoteExtensions.kt | 67 ++++++++++--------- .../base/note/recycler/NoteRecyclerItem.kt | 3 +- .../security/controller/AppLockController.kt | 2 +- .../main/java/com/maubis/markdown/Markdown.kt | 23 +++++-- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 8679f507..65faa2cf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -16,12 +16,13 @@ import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.* import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_DISTRACTION_FREE import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.sMarkdownEnabledHome import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID import com.maubis.scarlet.base.support.ui.ThemedActivity import java.util.* @@ -51,36 +52,41 @@ fun Note.getFullTextForDirectMarkdownRender(): String { return text } -fun Note.getMarkdownForListView(isMarkdownEnabled: Boolean): CharSequence { +fun Note.getMarkdownForListView(): CharSequence { var text = getFullTextForDirectMarkdownRender() if (sInternalShowUUID) { text = "`$uuid`\n\n$text" } - return when { - isMarkdownEnabled -> Markdown.renderWithCustomFormatting(text, true) { spannable, spanInfo -> - val s = spanInfo.start - val e = spanInfo.end - when (spanInfo.markdownType) { - MarkdownType.HEADING_1 -> { - spannable.relativeSize(1.2f, s, e) - .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) - .bold(s, e) - true - } - MarkdownType.HEADING_2 -> { - spannable.relativeSize(1.1f, s, e) - .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) - .bold(s, e) - true - } - MarkdownType.CHECKLIST_CHECKED -> { - spannable.strike(s, e) - true - } - else -> false + + if (sMarkdownEnabledHome) { + return markdownFormatForList(text) + } + return text +} + +internal fun markdownFormatForList(text: String): CharSequence { + return Markdown.renderWithCustomFormatting(text, true) { spannable, spanInfo -> + val s = spanInfo.start + val e = spanInfo.end + when (spanInfo.markdownType) { + MarkdownType.HEADING_1 -> { + spannable.relativeSize(1.2f, s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) + true + } + MarkdownType.HEADING_2 -> { + spannable.relativeSize(1.1f, s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) + true } + MarkdownType.CHECKLIST_CHECKED -> { + spannable.strike(s, e) + true + } + else -> false } - else -> text } } @@ -152,18 +158,19 @@ fun Note.getImageFile(): String { fun Note.getFullText(): String { val fullText = getFormats().map { it -> it.markdownText }.joinToString(separator = "\n").trim() - if (BuildConfig.DEBUG) { + if (sInternalShowUUID) { return "`$uuid`\n$fullText" } return fullText } -fun Note.getLockedAwareTextForHomeList(isMarkdownEnabled: Boolean): CharSequence { +fun Note.getLockedAwareTextForHomeList(): CharSequence { + val lockedText = "******************\n***********\n****************" val text = when { - this.locked -> "******************\n***********\n****************" - else -> getMarkdownForListView(isMarkdownEnabled) + this.locked && !sMarkdownEnabledHome -> "${getTitleForSharing()}\n$lockedText" + this.locked -> markdownFormatForList("# ${getTitleForSharing()}\n\n```\n$lockedText\n```") + else -> getMarkdownForListView() } - return text } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt index 170fdc84..2e03c8f5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt @@ -17,10 +17,9 @@ import com.maubis.scarlet.base.support.ui.ColorUtil class NoteRecyclerItem(context: Context, val note: Note) : RecyclerItem() { private val isLightShaded = ColorUtil.isLightColored(note.color) - private val isMarkdownEnabled = sEditorMarkdownEnabled && sMarkdownEnabledHome val lineCount = sNoteItemLineCount - val description = note.getLockedAwareTextForHomeList(isMarkdownEnabled) + val description = note.getLockedAwareTextForHomeList() val descriptionColor = when (isLightShaded) { true -> ContextCompat.getColor(context, R.color.dark_tertiary_text) false -> ContextCompat.getColor(context, R.color.light_primary_text) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt b/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt index 7f6359d4..91f3defe 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt @@ -13,7 +13,7 @@ object AppLockController { val deltaSinceLastUnlock = SystemClock.uptimeMillis() - sLastLoginTimeMs // unlock stays 10 minutes - if (deltaSinceLastUnlock > 1000 * 60 * 10) { + if (sLastLoginTimeMs == 0L || deltaSinceLastUnlock > 1000 * 60 * 10) { return true } diff --git a/markdown/src/main/java/com/maubis/markdown/Markdown.kt b/markdown/src/main/java/com/maubis/markdown/Markdown.kt index 268c182e..c26d32ff 100644 --- a/markdown/src/main/java/com/maubis/markdown/Markdown.kt +++ b/markdown/src/main/java/com/maubis/markdown/Markdown.kt @@ -42,16 +42,25 @@ object Markdown { val textBuilder = StringBuilder() val formats = ArrayList() segments.forEach { - val inliner = TextInliner(if (stripDelimiter) it.strip() else it.text()).get() - val strippedText = inliner.contentText(stripDelimiter) - val finalIndex = currentIndex + strippedText.length + val finalIndex: Int + val strippedText: String + when { + it.type() == MarkdownSegmentType.CODE -> { + strippedText = if (stripDelimiter) it.strip() else it.text() + finalIndex = currentIndex + strippedText.length + formats.add(SpanInfo(map(it.type()), currentIndex, finalIndex)) + } + else -> { + val inliner = TextInliner(if (stripDelimiter) it.strip() else it.text()).get() + strippedText = inliner.contentText(stripDelimiter) + finalIndex = currentIndex + strippedText.length - formats.add(SpanInfo(map(it.type()), currentIndex, finalIndex)) - if (it.type() != MarkdownSegmentType.CODE) { - formats.addAll(inliner.allContentSpans(stripDelimiter, currentIndex)) + formats.add(SpanInfo(map(it.type()), currentIndex, finalIndex)) + formats.addAll(inliner.allContentSpans(stripDelimiter, currentIndex)) + } } - currentIndex = finalIndex + 1 + currentIndex = finalIndex + 1 textBuilder.append(strippedText) textBuilder.append("\n") } From 8885bc019a68d11a4902a341de85c1745ced9b79 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 29 Jul 2019 22:27:40 +0100 Subject: [PATCH 049/134] Enter Pincode New Sheet UI --- .../sheet/BackupSettingsOptionsBottomSheet.kt | 17 +- .../scarlet/base/note/NoteExtensions.kt | 16 +- .../note/actions/NoteOptionsBottomSheet.kt | 20 +- .../base/note/recycler/NoteRecyclerHolder.kt | 23 +- .../sheet/SelectedNoteOptionsBottomSheet.kt | 17 +- .../sheets/EnterPincodeBottomSheet.kt | 288 ------------------ .../security/sheets/NoPincodeBottomSheet.kt | 8 +- .../security/sheets/PincodeBottomSheet.kt | 227 ++++++++++++++ .../sheet/SecurityOptionsBottomSheet.kt | 81 ++--- .../sheet/SettingsOptionsBottomSheet.kt | 2 +- .../main/res/layout/bottom_sheet_pin_code.xml | 111 ------- base/src/main/res/values/strings.xml | 4 +- 12 files changed, 293 insertions(+), 521 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt delete mode 100644 base/src/main/res/layout/bottom_sheet_pin_code.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt index f9bb9e70..7d69bb46 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt @@ -8,7 +8,7 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.export.support.PermissionUtils -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem @@ -91,16 +91,9 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { openSheet(activity, ExportNotesBottomSheet()) return } - EnterPincodeBottomSheet.openUnlockSheet( - activity as ThemedActivity, - object : EnterPincodeBottomSheet.PincodeSuccessListener { - override fun onFailure() { - openExportSheet(activity) - } - - override fun onSuccess() { - openSheet(activity, ExportNotesBottomSheet()) - } - }) + openUnlockSheet( + activity = activity as ThemedActivity, + onUnlockSuccess = { openSheet(activity, ExportNotesBottomSheet()) }, + onUnlockFailure = { openExportSheet(activity) }) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 65faa2cf..f231f50d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -7,7 +7,6 @@ import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.MarkdownConfig import com.maubis.markdown.spannable.* -import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -21,7 +20,7 @@ import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_DISTRACTION_FRE import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.sMarkdownEnabledHome import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID import com.maubis.scarlet.base.support.ui.ThemedActivity @@ -244,15 +243,10 @@ fun Note.mark(context: Context, noteState: NoteState) { fun Note.edit(context: Context) { if (this.locked) { if (context is ThemedActivity) { - EnterPincodeBottomSheet.openUnlockSheet(context, object : EnterPincodeBottomSheet.PincodeSuccessListener { - override fun onFailure() { - edit(context) - } - - override fun onSuccess() { - openEdit(context) - } - }) + openUnlockSheet( + activity = context, + onUnlockSuccess = { openEdit(context) }, + onUnlockFailure = { edit(context) }) } return } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 0cf870ac..ccd15605 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -14,7 +14,7 @@ import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getNoteState import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet + import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteNotePermanentlySheet import com.maubis.scarlet.base.note.* @@ -27,6 +27,7 @@ import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity import com.maubis.scarlet.base.note.tag.sheet.TagChooserBottomSheet import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler +import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.support.option.OptionsItem @@ -272,15 +273,14 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.unlock_note, icon = R.drawable.ic_action_unlock, listener = View.OnClickListener { - EnterPincodeBottomSheet.openUnlockSheet( - activity, - object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - note.locked = false - activity.updateNote(note) - dismiss() - } - }) + openUnlockSheet( + activity = activity, + onUnlockSuccess = { + note.locked = false + activity.updateNote(note) + dismiss() + }, + onUnlockFailure = { }) }, visible = note.locked )) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt index 23236da4..2d44806f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt @@ -5,12 +5,10 @@ import android.os.Bundle import android.view.View import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet +import com.maubis.scarlet.base.note.* + import com.maubis.scarlet.base.note.actions.NoteOptionsBottomSheet -import com.maubis.scarlet.base.note.copy -import com.maubis.scarlet.base.note.edit -import com.maubis.scarlet.base.note.share -import com.maubis.scarlet.base.note.view +import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.support.ui.ThemedActivity class NoteRecyclerHolder(context: Context, view: View) : NoteRecyclerViewHolderBase(context, view) { @@ -47,17 +45,10 @@ class NoteRecyclerHolder(context: Context, view: View) : NoteRecyclerViewHolderB private fun actionOrUnlockNote(data: Note, runnable: Runnable) { if (context is ThemedActivity && data.locked) { - EnterPincodeBottomSheet.openUnlockSheet( - context as ThemedActivity, - object : EnterPincodeBottomSheet.PincodeSuccessListener { - override fun onFailure() { - actionOrUnlockNote(data, runnable) - } - - override fun onSuccess() { - runnable.run() - } - }) + openUnlockSheet( + activity = context as ThemedActivity, + onUnlockSuccess = { runnable.run() }, + onUnlockFailure = { actionOrUnlockNote(data, runnable) }) return } else if (data.locked) { return diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt index 66ceb414..d1658eb9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt @@ -9,11 +9,11 @@ import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.sectionPreservingSort import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getFormats -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.folder.sheet.SelectedFolderChooseOptionsBottomSheet import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity import com.maubis.scarlet.base.note.tag.sheet.SelectedTagChooserBottomSheet +import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase @@ -266,14 +266,13 @@ class SelectedNoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() return@OnClickListener } - EnterPincodeBottomSheet.openUnlockSheet( - activity, - object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - listener() - dismiss() - } - }) + openUnlockSheet( + activity = activity, + onUnlockSuccess = { + listener() + dismiss() + }, + onUnlockFailure = {}) } companion object { diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt deleted file mode 100644 index 415ce1ad..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/EnterPincodeBottomSheet.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.maubis.scarlet.base.security.sheets - -import android.app.Dialog -import android.content.DialogInterface -import android.text.Editable -import android.text.TextWatcher -import android.view.KeyEvent -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView -import com.github.ajalt.reprint.core.AuthenticationFailureReason -import com.github.ajalt.reprint.core.AuthenticationListener -import com.github.ajalt.reprint.core.Reprint -import com.github.bijoysingh.starter.util.LocaleManager -import com.maubis.scarlet.base.MainActivity -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled -import com.maubis.scarlet.base.settings.sheet.sSecurityCode -import com.maubis.scarlet.base.settings.sheet.sSecurityFingerprintEnabled -import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment - - -class EnterPincodeBottomSheet : ThemedBottomSheetFragment() { - - var listener: PincodeListener? = null - - override fun getBackgroundView(): Int { - return R.id.container_layout - } - - override fun setupView(dialog: Dialog?) { - super.setupView(dialog) - if (dialog == null) { - return - } - - if (listener == null) { - dismiss() - } - - val title = dialog.findViewById(R.id.options_title) - val action = dialog.findViewById(R.id.action_button) - val enterPin = dialog.findViewById(R.id.enter_pin) - val pinLength = dialog.findViewById(R.id.pin_length) - val fingerprint = dialog.findViewById(R.id.fingerprint) - val removeBtn = dialog.findViewById(R.id.action_remove_button) - - title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterPin.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - - val hintColor = ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT) - enterPin.setHintTextColor(hintColor) - pinLength.setTextColor(hintColor) - - title.setText(listener!!.getTitle()) - action.setText(listener!!.getActionTitle()) - fingerprint.visibility = if (listener!!.isFingerprintEnabled()) View.VISIBLE else View.INVISIBLE - enterPin.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(p0: Editable?) { - // Ignore - } - - override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { - // Ignore - } - - override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { - if (p0 == null) { - return - } - if (p0.length > 4) { - enterPin.setText(p0.substring(0, 4)) - return - } - val text = LocaleManager.toString(p0.length) + " / " + LocaleManager.toString(PIN_LENGTH) - pinLength.text = text - } - }) - removeBtn.setOnClickListener { - listener!!.onRemoveButtonClick() - dismiss() - } - removeBtn.visibility = if (listener!!.isRemoveButtonEnabled()) View.VISIBLE else View.GONE - - enterPin.setOnEditorActionListener { _, actionId, event -> - if (event == null) { - if (actionId != EditorInfo.IME_ACTION_DONE && actionId != EditorInfo.IME_ACTION_NEXT) { - return@setOnEditorActionListener false - } - } else if (actionId == EditorInfo.IME_NULL || actionId == KeyEvent.KEYCODE_ENTER) { - if (event.getAction() != KeyEvent.ACTION_DOWN) { - return@setOnEditorActionListener true - } - } else { - return@setOnEditorActionListener false - } - - if (enterPin.length() != PIN_LENGTH) { - return@setOnEditorActionListener false - } - - listener!!.onPasswordRequested(enterPin.text.toString()) - dismiss() - return@setOnEditorActionListener true - } - - action.setOnClickListener { - val pinCode = enterPin.text.toString() - if (enterPin.length() != PIN_LENGTH) { - return@setOnClickListener - } - - listener!!.onPasswordRequested(pinCode) - dismiss() - } - - if (listener!!.isFingerprintEnabled()) { - Reprint.authenticate(object : AuthenticationListener { - override fun onSuccess(moduleTag: Int) { - listener!!.onSuccess() - dismiss() - } - - override fun onFailure( - failureReason: AuthenticationFailureReason?, - fatal: Boolean, - errorMessage: CharSequence?, - moduleTag: Int, - errorCode: Int) { - // Ignore - } - }) - } - makeBackgroundTransparent(dialog, R.id.root_layout) - } - - override fun onDismiss(dialog: DialogInterface?) { - super.onDismiss(dialog) - Reprint.cancelAuthentication() - } - - override fun onCancel(dialog: DialogInterface?) { - super.onCancel(dialog) - Reprint.cancelAuthentication() - } - - override fun getLayout(): Int = R.layout.bottom_sheet_pin_code - - override fun getBackgroundCardViewIds(): Array = arrayOf(R.id.enter_code_card) - - companion object { - - const val PIN_LENGTH = 4 - - fun openSheet(activity: ThemedActivity, listener: PincodeListener) { - val sheet = EnterPincodeBottomSheet() - - sheet.listener = listener - sheet.show(activity.supportFragmentManager, sheet.tag) - } - - fun openCreateSheet( - activity: ThemedActivity, - listener: PincodeSuccessOnlyListener) { - openSheet(activity, object : PincodeListener { - override fun getTitle(): Int = R.string.security_sheet_enter_new_pin_title - - override fun getActionTitle(): Int = R.string.security_sheet_button_set - - override fun isFingerprintEnabled(): Boolean = false - - override fun isRemoveButtonEnabled(): Boolean = true - - override fun onRemoveButtonClick() { - sSecurityCode = "" - sNoPinSetupNoticeShown = false - listener.onSuccess() - - if (activity is MainActivity) - activity.setupData() - } - - override fun onPasswordRequested(password: String) { - sSecurityCode = password - listener.onSuccess() - } - - override fun onSuccess() { - } - }) - } - - fun openVerifySheet( - activity: ThemedActivity, - listener: PincodeSuccessListener) { - openUnlockSheetBase( - activity, - listener, - R.string.security_sheet_enter_current_pin_title, - R.string.security_sheet_button_verify - ) - } - - fun openUnlockSheet( - activity: ThemedActivity, - listener: PincodeSuccessOnlyListener) { - if (!hasPinCodeEnabled()) { - if (sNoPinSetupNoticeShown) { - listener.onSuccess() - return - } - com.maubis.scarlet.base.support.sheets.openSheet(activity, NoPincodeBottomSheet().apply { - this.onSuccess = { listener.onSuccess() } - }) - return - } - - openUnlockSheetBase( - activity, - listener, - R.string.security_sheet_enter_pin_to_unlock_title, - R.string.security_sheet_button_unlock - ) - } - - private fun openUnlockSheetBase( - activity: ThemedActivity, - listener: PincodeSuccessOnlyListener, - title: Int, - actionTitle: Int) { - openSheet(activity, object : PincodeListener { - override fun getTitle(): Int = title - - override fun getActionTitle(): Int = actionTitle - - override fun isFingerprintEnabled(): Boolean { - return Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled - } - - override fun onPasswordRequested(password: String) { - val currentPassword = sSecurityCode - if (currentPassword != "" && currentPassword == password) { - listener.onSuccess() - } else if (listener is PincodeSuccessListener) { - listener.onFailure() - } - } - - override fun isRemoveButtonEnabled(): Boolean = false - - override fun onRemoveButtonClick() { - } - - override fun onSuccess() { - listener.onSuccess() - } - }) - } - } - - interface PincodeSuccessOnlyListener { - fun onSuccess() - } - - interface PincodeSuccessListener : PincodeSuccessOnlyListener { - - fun onFailure() - } - - interface PincodeListener : PincodeSuccessOnlyListener { - fun getTitle(): Int - - fun getActionTitle(): Int - - fun isFingerprintEnabled(): Boolean - - fun isRemoveButtonEnabled(): Boolean - - fun onPasswordRequested(password: String): Unit - - fun onRemoveButtonClick(): Unit - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt index 7f13b9eb..b9afba74 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt @@ -40,11 +40,9 @@ class NoPincodeBottomSheet : LithoBottomSheet() { .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.no_pincode_sheet_set_up) .onPrimaryClick { - EnterPincodeBottomSheet.openCreateSheet(activity, object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - // Ignore this - } - }) + openCreateSheet( + activity = activity, + onCreateSuccess = {}) dismiss() } .secondaryActionRes(R.string.no_pincode_sheet_dont_ask) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt new file mode 100644 index 00000000..12506763 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -0,0 +1,227 @@ +package com.maubis.scarlet.base.security.sheets + +import android.app.Dialog +import android.text.InputType +import android.text.Layout +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.EditText +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.TextChangedEvent +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.github.ajalt.reprint.core.Reprint +import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled +import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled +import com.maubis.scarlet.base.settings.sheet.sSecurityCode +import com.maubis.scarlet.base.settings.sheet.sSecurityFingerprintEnabled +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.specs.EmptySpec +import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.ui.ThemedActivity + +data class PincodeSheetData( + val title: Int, + val actionTitle: Int, + val onSuccess: () -> Unit, + val onFailure: () -> Unit = {}, + val isFingerprintEnabled: Boolean = false, + val onActionClicked: (String) -> Unit = {password -> + when { + password != "" && password == sSecurityCode -> onSuccess() + else -> onFailure() + } + }, + val isRemoveButtonEnabled: Boolean = false, + val onRemoveButtonClick: () -> Unit = {}) + +@LayoutSpec +object PincodeSheetViewSpec { + + private var passcodeEntered = "" + + @OnCreateLayout + fun onCreate(context: ComponentContext, @Prop data: PincodeSheetData): Component { + val component = Column.create(context) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child(getLithoBottomSheetTitle(context) + .textRes(data.title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child(Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textRes(R.string.app_lock_details) + .marginDip(YogaEdge.BOTTOM, 16f) + .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .child(EditText.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .textSizeRes(R.dimen.font_size_xlarge) + .minWidthDip(128f) + .maxLength(4) + .alignSelf(YogaAlign.CENTER) + .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_OPEN_SANS) + .textColor(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .paddingDip(YogaEdge.HORIZONTAL, 22f) + .paddingDip(YogaEdge.VERTICAL, 6f) + .marginDip(YogaEdge.VERTICAL, 8f) + .textChangedEventHandler(PincodeSheetView.onTextChangeListener(context))) + .child(Row.create(context) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 8f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child( + when { + data.isFingerprintEnabled -> Image.create(context) + .drawableRes(R.drawable.ic_option_fingerprint) + .heightDip(36f) + else -> null + } + ) + .child( + when { + data.isRemoveButtonEnabled -> Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(instance.themeController().get(ThemeColorType.HINT_TEXT)) + .textRes(R.string.security_sheet_button_remove) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .typeface(CoreConfig.FONT_MONSERRAT) + .clickHandler(PincodeSheetView.onRemoveClick(context)) + else -> null + } + ) + .child(EmptySpec.create(context).flexGrow(1f)) + .child(Text.create(context) + .backgroundRes(R.drawable.accent_rounded_bg) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(data.actionTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .typeface(CoreConfig.FONT_MONSERRAT) + .clickHandler(PincodeSheetView.onActionClick(context)))) + return component.build() + } + + @OnEvent(TextChangedEvent::class) + fun onTextChangeListener(context: ComponentContext, @FromEvent text: String) { + passcodeEntered = text + } + + @OnEvent(ClickEvent::class) + fun onActionClick(context: ComponentContext, + @Prop data: PincodeSheetData, + @Prop dismiss: () -> Unit) { + data.onActionClicked(passcodeEntered) + passcodeEntered = "" + dismiss() + } + + @OnEvent(ClickEvent::class) + fun onRemoveClick(context: ComponentContext, + @Prop data: PincodeSheetData, + @Prop dismiss: () -> Unit) { + data.onRemoveButtonClick() + passcodeEntered = "" + dismiss() + } +} + +class PincodeBottomSheet : LithoBottomSheet() { + var data = PincodeSheetData( + title = R.string.no_pincode_sheet_title, + actionTitle = R.string.no_pincode_sheet_details, + onSuccess = {}) + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + return PincodeSheetView.create(componentContext) + .data(data) + .dismiss { dismiss() } + .build() + } +} + +fun openCreateSheet( + activity: ThemedActivity, + onCreateSuccess: () -> Unit) { + + openSheet(activity, PincodeBottomSheet().apply { + data = PincodeSheetData( + title = R.string.security_sheet_enter_new_pin_title, + actionTitle = R.string.security_sheet_button_set, + isFingerprintEnabled = false, + isRemoveButtonEnabled = true, + onRemoveButtonClick = { + sSecurityCode = "" + sSecurityAppLockEnabled = false + sNoPinSetupNoticeShown = false + onCreateSuccess() + + if (activity is MainActivity) { + activity.setupData() + } + }, + onActionClicked = { password: String -> + if (password.length == 4 && password.toIntOrNull() !== null) { + sSecurityCode = password + onCreateSuccess() + } + }, + onSuccess = {} + ) + }) +} + +fun openVerifySheet( + activity: ThemedActivity, + onVerifySuccess: () -> Unit, + onVerifyFailure: () -> Unit = {}) { + openSheet(activity, PincodeBottomSheet().apply { + data = PincodeSheetData( + title = R.string.security_sheet_enter_current_pin_title, + actionTitle = R.string.security_sheet_button_verify, + onSuccess = onVerifySuccess, + onFailure = onVerifyFailure, + isFingerprintEnabled = Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled + ) + }) +} + +fun openUnlockSheet( + activity: ThemedActivity, + onUnlockSuccess: () -> Unit, + onUnlockFailure: () -> Unit) { + if (!hasPinCodeEnabled()) { + if (sNoPinSetupNoticeShown) { + onUnlockSuccess() + return + } + openSheet(activity, NoPincodeBottomSheet().apply { + this.onSuccess = onUnlockSuccess + }) + return + } + + openSheet(activity, PincodeBottomSheet().apply { + data = PincodeSheetData( + title = R.string.security_sheet_enter_pin_to_unlock_title, + actionTitle = R.string.security_sheet_button_unlock, + onSuccess = onUnlockSuccess, + onFailure = onUnlockFailure, + isFingerprintEnabled = Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled + ) + }) +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index 551fd9f1..534ec6d8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -7,9 +7,8 @@ import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet.Companion.openCreateSheet -import com.maubis.scarlet.base.security.sheets.EnterPincodeBottomSheet.Companion.openVerifySheet +import com.maubis.scarlet.base.security.sheets.openCreateSheet +import com.maubis.scarlet.base.security.sheets.openVerifySheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.ui.ThemedActivity @@ -32,8 +31,8 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.security_option_title override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { + val activity = context as ThemedActivity val options = ArrayList() - options.add(LithoOptionsItem( title = R.string.security_option_set_pin_code, subtitle = R.string.security_option_set_pin_code_subtitle, @@ -54,12 +53,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_option_security, listener = { when { - hasPinCodeEnabled() -> openVerifyPasswordDialog( - object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - sSecurityAppLockEnabled = !sSecurityAppLockEnabled - reset(componentContext.androidContext, dialog) - } + hasPinCodeEnabled() -> openVerifySheet( + activity = activity, + onVerifySuccess = { + sSecurityAppLockEnabled = !sSecurityAppLockEnabled + reset(componentContext.androidContext, dialog) } ) else -> openCreatePasswordDialog(dialog) @@ -77,12 +75,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_option_fingerprint, listener = { when { - hasPinCodeEnabled() -> openVerifyPasswordDialog( - object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - sSecurityFingerprintEnabled = false - reset(componentContext.androidContext, dialog) - } + hasPinCodeEnabled() -> openVerifySheet( + activity = activity, + onVerifySuccess = { + sSecurityFingerprintEnabled = false + reset(componentContext.androidContext, dialog) } ) else -> { @@ -101,12 +98,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_option_fingerprint, listener = { when { - hasPinCodeEnabled() -> openVerifyPasswordDialog( - object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - sSecurityFingerprintEnabled = true - reset(componentContext.androidContext, dialog) - } + hasPinCodeEnabled() -> openVerifySheet( + activity = activity, + onVerifySuccess = { + sSecurityFingerprintEnabled = true + reset(componentContext.androidContext, dialog) } ) else -> { @@ -123,50 +119,23 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { fun openCreatePasswordDialog(dialog: Dialog) { val activity = context as ThemedActivity openCreateSheet( - activity, - object : EnterPincodeBottomSheet.PincodeSuccessOnlyListener { - override fun onSuccess() { - reset(dialog.context, dialog) - } - }) + activity = activity, + onCreateSuccess = { reset(dialog.context, dialog) }) } fun openResetPasswordDialog(dialog: Dialog) { val activity = context as ThemedActivity openVerifySheet( activity, - object : EnterPincodeBottomSheet.PincodeSuccessListener { - override fun onFailure() { - openResetPasswordDialog(dialog) - } - - override fun onSuccess() { - openCreatePasswordDialog(dialog) - } - }) - } - - fun openVerifyPasswordDialog(listener: EnterPincodeBottomSheet.PincodeSuccessOnlyListener) { - val activity = context as ThemedActivity - openVerifySheet( - activity, - object : EnterPincodeBottomSheet.PincodeSuccessListener { - override fun onFailure() { - - } - - override fun onSuccess() { - listener.onSuccess() - } + onVerifySuccess = { + openCreatePasswordDialog(dialog) + }, + onVerifyFailure = { + openResetPasswordDialog(dialog) }) } companion object { - fun openSheet(activity: MainActivity) { - val sheet = SecurityOptionsBottomSheet() - sheet.show(activity.supportFragmentManager, sheet.tag) - } - fun hasPinCodeEnabled(): Boolean { return !TextUtils.isNullOrEmpty(sSecurityCode) } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index 8897b43f..2e02ca92 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -81,7 +81,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.home_option_security_subtitle, icon = R.drawable.ic_option_security, listener = { - SecurityOptionsBottomSheet.openSheet(activity) + openSheet(activity, SecurityOptionsBottomSheet()) dismiss() } )) diff --git a/base/src/main/res/layout/bottom_sheet_pin_code.xml b/base/src/main/res/layout/bottom_sheet_pin_code.xml deleted file mode 100644 index f1e05a48..00000000 --- a/base/src/main/res/layout/bottom_sheet_pin_code.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 4ad3656a..5d11116e 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -504,8 +504,8 @@ Unlock App - Enter PIN or use fingerprint to unlock - Enter PIN to unlock + Enter 4 digit PIN or use fingerprint to unlock + Enter 4 digit PIN to unlock From 82eeee22a83c4bd57e674c45657a30b340b1e6c1 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 29 Jul 2019 23:12:00 +0100 Subject: [PATCH 050/134] Adding option to unlock notes only once --- app/build.gradle | 4 +- .../sheet/BackupSettingsOptionsBottomSheet.kt | 3 +- .../scarlet/base/note/NoteExtensions.kt | 15 ++++-- .../base/note/recycler/NoteRecyclerItem.kt | 2 + .../recycler/NoteRecyclerViewHolderBase.kt | 6 +++ .../base/security/activity/AppLockActivity.kt | 8 ++- .../security/controller/AppLockController.kt | 31 ------------ .../security/controller/PinLockController.kt | 49 +++++++++++++++++++ .../security/sheets/PincodeBottomSheet.kt | 14 ++++-- .../sheet/SecurityOptionsBottomSheet.kt | 44 +++++++++++------ .../base/support/ui/SecuredActivity.kt | 4 +- base/src/main/res/layout/item_note.xml | 8 +++ .../main/res/layout/item_note_staggered.xml | 8 +++ base/src/main/res/values/strings.xml | 2 + build.gradle | 4 +- 15 files changed, 137 insertions(+), 65 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/security/controller/PinLockController.kt diff --git a/app/build.gradle b/app/build.gradle index d9b70ebd..44f3f2ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 135 - versionName '7.0.4' + versionCode 136 + versionName '7.0.6' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt index 7d69bb46..38d65ace 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt @@ -8,6 +8,7 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.export.support.PermissionUtils +import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet @@ -87,7 +88,7 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { } private fun openExportSheet(activity: MainActivity) { - if (!SecurityOptionsBottomSheet.hasPinCodeEnabled()) { + if (!isPinCodeEnabled()) { openSheet(activity, ExportNotesBottomSheet()) return } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index f231f50d..183910d9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -20,9 +20,11 @@ import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_DISTRACTION_FRE import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.security.controller.PinLockController.needsLockCheck import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.sMarkdownEnabledHome import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID +import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled import com.maubis.scarlet.base.support.ui.ThemedActivity import java.util.* import kotlin.collections.ArrayList @@ -163,14 +165,17 @@ fun Note.getFullText(): String { return fullText } +fun Note.isNoteLockedButAppUnlocked(): Boolean { + return this.locked && !needsLockCheck() && sSecurityAppLockEnabled +} + fun Note.getLockedAwareTextForHomeList(): CharSequence { val lockedText = "******************\n***********\n****************" - val text = when { - this.locked && !sMarkdownEnabledHome -> "${getTitleForSharing()}\n$lockedText" - this.locked -> markdownFormatForList("# ${getTitleForSharing()}\n\n```\n$lockedText\n```") - else -> getMarkdownForListView() + return when { + isNoteLockedButAppUnlocked() || !this.locked -> getMarkdownForListView() + !sMarkdownEnabledHome -> "${getTitleForSharing()}\n$lockedText" + else -> markdownFormatForList("# ${getTitleForSharing()}\n\n```\n$lockedText\n```") } - return text } fun Note.getDisplayTime(): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt index 2e03c8f5..ca56e9ea 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt @@ -9,8 +9,10 @@ import com.maubis.scarlet.base.core.note.getReminderV2 import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.creation.sheet.sEditorMarkdownEnabled +import com.maubis.scarlet.base.security.controller.PinLockController import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.sMarkdownEnabledHome import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount +import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ColorUtil diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt index cbabf065..127ce2b6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt @@ -14,6 +14,7 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.note.isNoteLockedButAppUnlocked import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.visibility import com.maubis.scarlet.base.support.utils.trim @@ -32,6 +33,7 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi protected val bottomLayout: View protected val pinIndicator: ImageView + protected val unlockIndicator: ImageView protected val reminderIndicator: ImageView protected val stateIndicator: ImageView protected val backupIndicator: ImageView @@ -46,6 +48,7 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi copy = view.findViewById(R.id.copy_button) moreOptions = view.findViewById(R.id.options_button) pinIndicator = view.findViewById(R.id.pin_icon) + unlockIndicator = view.findViewById(R.id.unlock_icon) reminderIndicator = view.findViewById(R.id.reminder_icon) edit = view.findViewById(R.id.edit_button) bottomLayout = view.findViewById(R.id.bottom_toolbar_layout) @@ -60,6 +63,7 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi setIndicators(item) setMetaText(item) + view.alpha = if (item.note.isNoteLockedButAppUnlocked()) 0.7f else 1.0f view.setOnClickListener { viewClick(item.note, extra) } view.setOnLongClickListener { viewLongClick(item.note, extra) @@ -102,11 +106,13 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi } NoteState.DEFAULT -> stateIndicator.visibility = GONE } + unlockIndicator.visibility = visibility(note.note.locked) pinIndicator.setColorFilter(note.indicatorColor) stateIndicator.setColorFilter(note.indicatorColor) reminderIndicator.setColorFilter(note.indicatorColor) backupIndicator.setColorFilter(note.indicatorColor) + unlockIndicator.setColorFilter(note.indicatorColor) } private fun setMetaText(note: NoteRecyclerItem) { diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt index 3e379bc8..df079621 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt @@ -1,16 +1,14 @@ package com.maubis.scarlet.base.security.activity import android.content.Context -import android.content.Intent import android.os.Bundle -import android.support.v7.app.AppCompatActivity import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView import com.github.ajalt.reprint.core.AuthenticationFailureReason import com.github.ajalt.reprint.core.AuthenticationListener import com.github.ajalt.reprint.core.Reprint -import com.maubis.scarlet.base.security.controller.AppLockController +import com.maubis.scarlet.base.security.controller.PinLockController import com.maubis.scarlet.base.settings.sheet.sSecurityCode import com.maubis.scarlet.base.settings.sheet.sSecurityFingerprintEnabled import com.maubis.scarlet.base.support.ui.ThemedActivity @@ -38,7 +36,7 @@ class AppLockActivity : ThemedActivity() { } .onClick { if (passCodeEntered.length == 4 && sSecurityCode == passCodeEntered) { - AppLockController.notifyAppLock() + PinLockController.notifyPinVerified() finish() } } @@ -51,7 +49,7 @@ class AppLockActivity : ThemedActivity() { passCodeEntered = "" Reprint.authenticate(object : AuthenticationListener { override fun onSuccess(moduleTag: Int) { - AppLockController.notifyAppLock() + PinLockController.notifyPinVerified() finish() } diff --git a/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt b/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt deleted file mode 100644 index 91f3defe..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/security/controller/AppLockController.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.maubis.scarlet.base.security.controller - -import android.os.SystemClock -import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled -import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled - -object AppLockController { - private var sLastLoginTimeMs = 0L - - fun needsAppLock(): Boolean { - if (hasPinCodeEnabled() && sSecurityAppLockEnabled) { - // App lock enabled - val deltaSinceLastUnlock = SystemClock.uptimeMillis() - sLastLoginTimeMs - - // unlock stays 10 minutes - if (sLastLoginTimeMs == 0L || deltaSinceLastUnlock > 1000 * 60 * 10) { - return true - } - - // reset lock time - notifyAppLock() - return false - } - return false - } - - fun notifyAppLock() { - sLastLoginTimeMs = SystemClock.uptimeMillis() - } - -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/security/controller/PinLockController.kt b/base/src/main/java/com/maubis/scarlet/base/security/controller/PinLockController.kt new file mode 100644 index 00000000..64f4ef88 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/security/controller/PinLockController.kt @@ -0,0 +1,49 @@ +package com.maubis.scarlet.base.security.controller + +import android.os.SystemClock +import com.github.bijoysingh.starter.util.TextUtils +import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled +import com.maubis.scarlet.base.settings.sheet.sSecurityAskPinAlways +import com.maubis.scarlet.base.settings.sheet.sSecurityCode + +object PinLockController { + private var sLastLoginTimeMs = 0L + + fun isPinCodeEnabled(): Boolean { + return !TextUtils.isNullOrEmpty(sSecurityCode) + } + + fun needsAppLock(): Boolean { + // App lock enabled + if (isPinCodeEnabled() && sSecurityAppLockEnabled) { + return needsLockCheckImpl() + } + return false + } + + fun needsLockCheck(): Boolean { + if (sSecurityAskPinAlways) { + return true + } + + return needsLockCheckImpl() + } + + private fun needsLockCheckImpl(): Boolean { + val deltaSinceLastUnlock = SystemClock.uptimeMillis() - sLastLoginTimeMs + + // unlock stays 10 minutes + if (sLastLoginTimeMs == 0L || deltaSinceLastUnlock > 1000 * 60 * 5) { + return true + } + + // reset lock time + notifyPinVerified() + return false + } + + fun notifyPinVerified() { + sLastLoginTimeMs = SystemClock.uptimeMillis() + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index 12506763..4098d205 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -17,7 +17,9 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet.Companion.hasPinCodeEnabled +import com.maubis.scarlet.base.security.controller.PinLockController +import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled +import com.maubis.scarlet.base.security.controller.PinLockController.needsLockCheck import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled import com.maubis.scarlet.base.settings.sheet.sSecurityCode import com.maubis.scarlet.base.settings.sheet.sSecurityFingerprintEnabled @@ -36,7 +38,10 @@ data class PincodeSheetData( val isFingerprintEnabled: Boolean = false, val onActionClicked: (String) -> Unit = {password -> when { - password != "" && password == sSecurityCode -> onSuccess() + password != "" && password == sSecurityCode -> { + PinLockController.notifyPinVerified() + onSuccess() + } else -> onFailure() } }, @@ -204,7 +209,7 @@ fun openUnlockSheet( activity: ThemedActivity, onUnlockSuccess: () -> Unit, onUnlockFailure: () -> Unit) { - if (!hasPinCodeEnabled()) { + if (!isPinCodeEnabled()) { if (sNoPinSetupNoticeShown) { onUnlockSuccess() return @@ -215,6 +220,9 @@ fun openUnlockSheet( return } + if (!needsLockCheck()) { + return onUnlockSuccess() + } openSheet(activity, PincodeBottomSheet().apply { data = PincodeSheetData( title = R.string.security_sheet_enter_pin_to_unlock_title, diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index 534ec6d8..639b3694 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -3,10 +3,9 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog import com.facebook.litho.ComponentContext import com.github.ajalt.reprint.core.Reprint -import com.github.bijoysingh.starter.util.TextUtils -import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.sheets.openCreateSheet import com.maubis.scarlet.base.security.sheets.openVerifySheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet @@ -16,6 +15,7 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity const val KEY_SECURITY_CODE = "KEY_SECURITY_CODE" const val KEY_FINGERPRINT_ENABLED = "KEY_FINGERPRINT_ENABLED" const val KEY_APP_LOCK_ENABLED = "app_lock_enabled" +const val KEY_ASK_PIN_ALWAYS = "ask_pin_always" var sSecurityCode: String get() = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") @@ -26,6 +26,9 @@ var sSecurityFingerprintEnabled: Boolean var sSecurityAppLockEnabled: Boolean get() = ApplicationBase.instance.store().get(KEY_APP_LOCK_ENABLED, false) set(value) = ApplicationBase.instance.store().put(KEY_APP_LOCK_ENABLED, value) +var sSecurityAskPinAlways: Boolean + get() = ApplicationBase.instance.store().get(KEY_ASK_PIN_ALWAYS, true) + set(value) = ApplicationBase.instance.store().put(KEY_ASK_PIN_ALWAYS, value) class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.security_option_title @@ -39,21 +42,21 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_option_security, listener = { when { - hasPinCodeEnabled() -> openResetPasswordDialog(dialog) + isPinCodeEnabled() -> openResetPasswordDialog(dialog) else -> openCreatePasswordDialog(dialog) } }, isSelectable = true, - selected = hasPinCodeEnabled() + selected = isPinCodeEnabled() )) options.add(LithoOptionsItem( title = R.string.security_option_lock_app, subtitle = R.string.security_option_lock_app_details, - icon = R.drawable.ic_option_security, + icon = R.drawable.ic_apps_white_48dp, listener = { when { - hasPinCodeEnabled() -> openVerifySheet( + isPinCodeEnabled() -> openVerifySheet( activity = activity, onVerifySuccess = { sSecurityAppLockEnabled = !sSecurityAppLockEnabled @@ -67,6 +70,25 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { selected = sSecurityAppLockEnabled )) + options.add(LithoOptionsItem( + title = R.string.security_option_ask_pin_always, + subtitle = R.string.security_option_ask_pin_always_details, + icon = R.drawable.ic_action_grid, + listener = { + when { + isPinCodeEnabled() -> openVerifySheet( + activity = activity, + onVerifySuccess = { + sSecurityAskPinAlways = !sSecurityAskPinAlways + reset(componentContext.androidContext, dialog) + } + ) + else -> openCreatePasswordDialog(dialog) + } + }, + isSelectable = true, + selected = sSecurityAskPinAlways + )) val hasFingerprint = Reprint.hasFingerprintRegistered() options.add(LithoOptionsItem( @@ -75,7 +97,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_option_fingerprint, listener = { when { - hasPinCodeEnabled() -> openVerifySheet( + isPinCodeEnabled() -> openVerifySheet( activity = activity, onVerifySuccess = { sSecurityFingerprintEnabled = false @@ -98,7 +120,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { icon = R.drawable.ic_option_fingerprint, listener = { when { - hasPinCodeEnabled() -> openVerifySheet( + isPinCodeEnabled() -> openVerifySheet( activity = activity, onVerifySuccess = { sSecurityFingerprintEnabled = true @@ -134,10 +156,4 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { openResetPasswordDialog(dialog) }) } - - companion object { - fun hasPinCodeEnabled(): Boolean { - return !TextUtils.isNullOrEmpty(sSecurityCode) - } - } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt index 214f0c77..2ab15160 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/SecuredActivity.kt @@ -2,12 +2,12 @@ package com.maubis.scarlet.base.support.ui import android.content.Intent import com.maubis.scarlet.base.security.activity.AppLockActivity -import com.maubis.scarlet.base.security.controller.AppLockController +import com.maubis.scarlet.base.security.controller.PinLockController abstract class SecuredActivity : ThemedActivity() { override fun onResume() { super.onResume() - if (AppLockController.needsAppLock()) { + if (PinLockController.needsAppLock()) { startActivity(Intent(this, AppLockActivity::class.java)) } } diff --git a/base/src/main/res/layout/item_note.xml b/base/src/main/res/layout/item_note.xml index 5c488354..125da1a8 100644 --- a/base/src/main/res/layout/item_note.xml +++ b/base/src/main/res/layout/item_note.xml @@ -80,6 +80,14 @@ android:src="@drawable/ic_action_backup_no" android:visibility="gone" /> + + + + Set a 4 digit PIN to lock notes Enable App Lock Use PIN to unlock the whole application + Always ask for PIN + Enter PIN for all notes, even if previously entered Unlock with Fingerprint The notes will unlock with the fingerprint Fingerprint disabled diff --git a/build.gradle b/build.gradle index 904ccbe9..f03f585c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 135 - ext.appconfig_version = '7.0.4' + ext.appconfig_version_code = 136 + ext.appconfig_version = '7.0.6' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From 247387998149a4d62e5379c39b41053210cf5034 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 29 Jul 2019 23:21:40 +0100 Subject: [PATCH 051/134] Adding pin and security FAQ --- faq/README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/faq/README.md b/faq/README.md index 8e1dfc70..9b41832c 100644 --- a/faq/README.md +++ b/faq/README.md @@ -2,4 +2,36 @@ It is a curated list of questions asked by users. We plan to add more questions as you ask more questions. So if you did not find your question? Add a question [here](https://github.com/BijoySingh/Scarlet-Notes/issues) with `[FAQ]` in the title or, email us at team.thecodershub@gmail.com with `[FAQ]` in the title. ## How to add notes? -Simply press the "Add Note" button in the bottom right of the home screen. You can then start adding your notes in the new screen. \ No newline at end of file +Simply press the "Add Note" button in the bottom right of the home screen. You can then start adding your notes in the new screen. + +## Locking Notes and Security +### How do I setup a new PIN? +**App newer than v7.0.5** +- Go to `App Settings > Security` +- Select `Pass Code` option + +**Older versions** +- Go to `App Settings > Note Preferences > Security` +- Select `Pass Code` option + +### Can I setup an App Lock? +**App newer than v7.0.5** +- Go to `App Settings > Security` +- Select `Enable App Lock` option + +### The app asks for a PIN repeatedly +**App newer than v7.0.5** +- Go to `App Settings > Security` +- Select `Always ask for PIN` option +_Note: If you have enabled app lock, the locked notes will become visible with this option enabled_ + +### How can I lock a note? +Long press a note (or open note options) and choose `Lock Note` + +### Do locked notes get backed up? +Yes, by default they will backed up. But you can disable backup for a note by choosing `Disable Backup` + +### Can I view a locked note? +Of course, if you click on a locked note, you can enter the PIN or scan the fingerprint to view the note. + + From 01d2ff1cfcefedeed7240c92fabb13ec4f75b0fd Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 30 Jul 2019 00:22:48 +0100 Subject: [PATCH 052/134] Removing unnecessary strings from resources --- base/src/main/res/values-ar/strings.xml | 71 --------------------- base/src/main/res/values-bn/strings.xml | 63 ------------------ base/src/main/res/values-de/strings.xml | 63 ------------------ base/src/main/res/values-es/strings.xml | 63 ------------------ base/src/main/res/values-fr/strings.xml | 63 ------------------ base/src/main/res/values-hi/strings.xml | 63 ------------------ base/src/main/res/values-it/strings.xml | 63 ------------------ base/src/main/res/values-iw/strings.xml | 63 ------------------ base/src/main/res/values-ja/strings.xml | 63 ------------------ base/src/main/res/values-ko/strings.xml | 63 ------------------ base/src/main/res/values-mr/strings.xml | 63 ------------------ base/src/main/res/values-ms/strings.xml | 63 ------------------ base/src/main/res/values-pa/strings.xml | 63 ------------------ base/src/main/res/values-pl/strings.xml | 63 ------------------ base/src/main/res/values-pt/strings.xml | 63 ------------------ base/src/main/res/values-ru/strings.xml | 71 --------------------- base/src/main/res/values-tr/strings.xml | 64 ------------------- base/src/main/res/values-zh-rCN/strings.xml | 66 ------------------- base/src/main/res/values-zh-rHK/strings.xml | 63 ------------------ base/src/main/res/values-zh-rMO/strings.xml | 63 ------------------ base/src/main/res/values-zh-rTW/strings.xml | 63 ------------------ base/src/main/res/values-zh/strings.xml | 63 ------------------ base/src/main/res/values/strings.xml | 69 -------------------- 23 files changed, 1475 deletions(-) diff --git a/base/src/main/res/values-ar/strings.xml b/base/src/main/res/values-ar/strings.xml index e8d51ac2..5c9165a0 100644 --- a/base/src/main/res/values-ar/strings.xml +++ b/base/src/main/res/values-ar/strings.xml @@ -4,24 +4,17 @@ حفظ استيراد بحث ملاحظات | | - البحث %s… جار المشاركة باستخدام … لا ملاحظات يبدو أنك لم تضف أي ملاحظات. انقر لإضافة ملاحظة. - إضافة ملاحظة … - إضافة ملاحظة سريعة … - اضف ملاحظة إضافة قائمة التحقق - الوضع الليلي - وضع اليوم تحرير مذكرة إنشاء إعلام فتح في نافذة منبثقة - حذف ملاحظة الحذف بشكل نهائي نسخ ملاحظة إرسال ملاحظة @@ -43,8 +36,6 @@ الهاء الحرة اختر الإجراء… - انقر لعرض الملاحظة في الوضع الليلي - انقر لعرض الملاحظة في وضع اليوم انقر لفتح ملاحظة للتحرير انقر لنسخ محتوى الملاحظة إلى الحافظة انقر لمشاركة محتوى المذكرة مع تطبيقات أخرى @@ -58,60 +49,26 @@ انقر لتغيير لون خلفية الملاحظة انقر على إلغاء أرشفة الملاحظة - ملاحظاتك | | الصفحة الرئيسية - كل من الملاحظات العادية والمفضلة لديك المفضلة - جميع الملاحظات المفضلة لديك أرشفة - جميع الملاحظات المؤرشفة مقفل - كل من الملاحظات الخاصة بك مؤمن - الكلمات - كل علامات الملاحظة الخاصة بك قمامة، يدمر، يهدم - جميع الملاحظات في المهملات إضافة محتوى… إضافة العنوان … - إضافة عنوان فرعي … إضافة نص مقتبس … اضافة عنصر… إضافة شفرة … انقر لإضافة صورة أو تغييرها - كتلة التنسيق - عنوان - عنوان فرعي - نص - اقتبس - الشفرة - قائمة تدقيق - صورة - الفاصل - - تنسيق تحديد السعر - بالخط العريض - المائل - أكد - إضراب - قائمة - خيارات والإعدادات واجهة والخبرة اختيار كيف يبدو التطبيق ويشعر. - ملاحظة التفضيلات - اختر ملاحظة وإعدادات أخرى. حول معرفة المزيد عنا والتطبيق. الأمان تأمين الملاحظات وخيارات الأمان - تمكين الوضع الليلي - تمكين المظهر المظلم كخيار افتراضي - تمكين وضع اليوم - تمكين موضوع الضوء بشكل افتراضي - إعدادات تخفيض السعر - اطلع على كيفية استخدام تخفيض السعر في الملاحظات تمكين تخطيط القائمة إظهار الملاحظات في عمود واحد تمكين تخطيط الشبكة @@ -131,8 +88,6 @@ معرفة المزيد عن مشروع مفتوح المصدر معدل ومراجعة أخبرنا أعرف كم كنت أحب التطبيق - ملء المسح - مساعدتنا على تحسين التطبيق من خلال إخبارنا ما تريد تثبيت برو التطبيق ثبِّت Pro Pro لإلغاء قفل الميزات ومطور الدعم تهاجر ملاحظات على القرمزي برو @@ -161,7 +116,6 @@ أدخل رقم تعريف شخصي جديد أدخل رقم التعريف الشخصي لإلغاء التأمين أدخل رقم التعريف الشخصي الحالي - ادخل الرمز التحقق فتح جلس @@ -176,7 +130,6 @@ السماح بتنسيق التضمين المنسق تخفيض السعر في قائمة الملاحظات تمكين تنسيق علامة التبويب في قائمة الملاحظات - أمثلة إعطاء الأذونات استيراد وتصدير تتطلب أذونات التخزين. يرجى إعطاء الإذن عند الطلب. @@ -191,7 +144,6 @@ شارك - معلومات عنا وأكثر معدل على اللعب المتجر تساهم نسخة التطبيق @@ -199,16 +151,12 @@ المكتبات مشروع المصدر المفتوح - حول مشروع المصدر المفتوح - ملاحظة تذكير تاريخ التذكير تذكير الوقت تكرار تكرار مرة واحدة فقط اليومي - العادة - بيتا ملاحظة الإخطارات تذكير وإنذارات @@ -241,10 +189,6 @@ تعرف ما هو الجديد في التحديثات الأخيرة من التطبيق ترجمه - انقر لإضافة ملاحظة جديدة - انقر لرؤية خيارات المنزل - القائمة الرئيسية لديها الملاحظات المفضلة لديك والأرشيف، وكذلك إعدادات التطبيق. يمكنك العثور على العلامات هنا أيضا. - نسخ حظر الإجراءات النصية صالة عرض @@ -299,9 +243,6 @@ تسجيل الدخول للحصول على النسخ الاحتياطي سحابة والمزامنة خروج تسجيل الخروج لإيقاف النسخ الاحتياطي سحابة - الدخول مع جوجل - تسجيل الدخول | - لقد سجلت الدخول فشل تسجيل الدخول إلى غوغل سياسة الخصوصية @@ -315,19 +256,10 @@ احصل على أحدث الميزات أولا \n \n ستتوفر بعض الميزات الإضافية فقط لمستخدمي برو - سبب تمكين مزامنة السحاب - التحميل والنسخ الاحتياطي من تغييرات الجهاز \n - \n - المزامنة بين أجهزة متعددة، والحصول على تحديثات سريعة \n - \n - حفظها بشكل آمن على خوادم غوغل فيريباس - مرحبا، - ملاحظات المادة الآن القرمزي! - البدء @@ -338,7 +270,6 @@ انقر لإضافة أو تغيير العلامات - عرض المزيد من الإجراءات حدد ملاحظات حدد إجراءات وتنفيذها على ملاحظات متعددة في وقت واحد @@ -365,8 +296,6 @@ دمج الملاحظات تصدير كما تخفيض السعر تصدير الملاحظات في شكل تخفيض السعر. (لا يمكنك استيراد هذه العودة إلى التطبيق) - مجلد التصدير - اختر مكان تخزين جميع الصادرات استخدام لون موضوع للخلفية استخدام لون المذكرة للخلفية مكن diff --git a/base/src/main/res/values-bn/strings.xml b/base/src/main/res/values-bn/strings.xml index 8acc85b9..ac3f64ac 100644 --- a/base/src/main/res/values-bn/strings.xml +++ b/base/src/main/res/values-bn/strings.xml @@ -6,17 +6,13 @@ ব্যবহার করে ভাগ করুন … কোন নোট নেই মনে হচ্ছে আপনি কোন নোট যোগ না করেছেন। একটি নোট যোগ করতে ক্লিক করুন - একটি নোট যোগ করো… - একটি দ্রুত নোট যোগ করুন … নোট সম্পাদনা করুন পপআপ খুলুন - নোট মুছুন অনুলিপি নোট নোট পাঠান নির্বাচন করুন… সামগ্রী যোগ করুন … শিরোলেখ জুড়ুন … - উপ শিরোনাম যোগ করুন … উদ্ধৃত পাঠ্য যোগ করুন … আইটেম যোগ করুন… কোড যুক্ত করুন … @@ -26,9 +22,6 @@ একটি স্তব্ধ গ্রিডে নোট দেখান মার্কড্ডন সহায়তা মাপদণ্ড সমর্থিত ফরম্যাটিং মঞ্জুরি দিন - উদাহরণ - মার্কডাউন সেটিংস - নোটগুলিতে মার্কাডাউন কিভাবে ব্যবহার করা যায় তা দেখুন লক নোট নোট আনলক করুন নিরাপত্তা @@ -43,7 +36,6 @@ নতুন PIN লিখুন আনলক করতে পিন লিখুন বর্তমান PIN লিখুন - কোড লিখুন যাচাই করুন আনলক করুন সেট @@ -68,7 +60,6 @@ ফাইলের জন্য এক্সপোর্ট করা হচ্ছে | সম্পন্ন ভাগ - আমাদের সম্পর্কে এবং আরও প্লে স্টোরে রেট দিন অবদান অনুসন্ধান নোটগুলি | @@ -76,16 +67,9 @@ অ্যাপ সংস্করণ অ্যাপ সম্পর্কে - দিন মোড রঙ পরিবর্তন করুন - দিন মোডে নোট দেখতে ট্যাপ করুন নোটের পটভূমির রং পরিবর্তন করতে আলতো চাপুন - নাইট মোড সক্ষম করুন - ডিফল্ট হিসাবে অন্ধকার থিমটি সক্ষম করুন - ডে মোড সক্ষম করুন - ডিফল্ট হিসাবে হালকা থিমটি সক্ষম করুন - রাত মোড চিরতরে মুছে দাও অব্যবহৃত নোট আর্কাইভ নোট @@ -93,7 +77,6 @@ চিহ্নিত করুন প্রিয় নোট পুনরুদ্ধার করুন আবর্জনা সরান - রাতের মোডে নোট দেখতে ট্যাপ করুন সম্পাদনা করার জন্য নোটটি খুলতে ট্যাপ করুন ক্লিপবোর্ডে নোট সামগ্রী কপি করতে আলতো চাপুন অন্যান্য অ্যাপ্লিকেশনে নোট সামগ্রী ভাগ করতে আলতো চাপুন @@ -105,21 +88,13 @@ নোট হিসাবে প্রিয় হিসাবে চিহ্নিত করতে আলতো চাপুন নোট আর্কাইভ করতে আলতো চাপুন নোট অযাচাই করার জন্য আলতো চাপুন - আপনার নোটস | বাড়ি - আপনার সব স্বাভাবিক এবং প্রিয় নোট প্রিয় - আপনার প্রিয় নোট সব আর্কাইভ করা - আপনার সংরক্ষণাগারভুক্ত সমস্ত নোট আবর্জনা - ট্র্যাশ আপনার সমস্ত নোট বিজ্ঞপ্তি তৈরি করুন নোট বিজ্ঞপ্তিগুলি লক - আপনার লক নোট সব - ট্যাগ - আপনার সমস্ত নোট ট্যাগ নতুন ট্যাগ যোগ করুন | ট্যাগ করা সম্পাদনা করুন | ট্যাগ লিখুন @@ -130,8 +105,6 @@ আমদানি নোট তালিকাতে মার্কডাউন নোটগুলির তালিকাতে মার্কড্ডা ফর্ম্যাটিং সক্ষম করুন - সার্ভে পূরণ - আপনার পছন্দ কি আমাদের বলার মাধ্যমে অ্যাপ্লিকেশন উন্নত করতে আমাদের সাহায্য করুন ট্যাগ পরিবর্তন করুন @@ -140,8 +113,6 @@ ওপেন সোর্স প্রকল্প সম্পর্কে আরও জানুন লাইব্রেরি ওপেন সোর্স প্রকল্প - ওপেন সোর্স প্রকল্প সম্পর্কে - নোট অনুস্মারক অনুস্মারক তারিখ অনুস্মারক সময় পুনরাবৃত্তি ফ্রিকোয়েন্সি @@ -156,8 +127,6 @@ ইন্টারফেস এবং অভিজ্ঞতা অ্যাপ্লিকেশন কেমন দেখায় এবং অনুভব করে তা চয়ন করুন। - নোট পছন্দসমূহ - নোট এবং অন্যান্য সেটিংস চয়ন করুন। সম্পর্কিত আমাদের এবং অ্যাপ সম্পর্কে আরও জানুন ডিফল্ট নোট রঙ @@ -192,19 +161,6 @@ নোট যোগ করুন চেকলিস্ট যোগ করুন - ফরম্যাটিং ব্লক করুন - শিরোনাম - উপ শিরোনাম - পাঠ - উদ্ধৃতি - কোড - চেকলিস্ট - মার্কডাউন ফরম্যাটিং - সাহসী - ইটালিক - আনডারলাইন করা - ধর্মঘট - তালিকা একটি নোট নির্বাচন করুন নোট নির্বাচন করুন সেটিংস @@ -218,29 +174,18 @@ কি নতুন অ্যাপ্লিকেশনের সাম্প্রতিক আপডেটে নতুন কি আছে তা জানুন অনুবাদ - একটি নতুন নোট যোগ করতে ক্লিক করুন - হোম বিকল্প দেখতে ক্লিক করুন - হোম মেনু আপনার প্রিয় এবং আর্কাইভ নোট, পাশাপাশি অ্যাপ্লিকেশন সেটিংস আছে। আপনি এখানে আপনার ট্যাগ খুঁজে পেতে পারেন। অ্যাপে সাইন ইন করুন ক্লাউড ব্যাকআপ এবং সিঙ্ক্রোনাইজেশনের জন্য সাইন ইন করুন সাইন আউট ক্লাউড ব্যাকআপ বন্ধ করতে সাইন আউট করুন - Google এর সাথে সাইন ইন করুন - সাইন ইন ইন … - আপনি সংযুক্ত আছেন Google লগইন ব্যর্থ গোপনীয়তা নীতি কন্টেন্ট জন্য অ্যাপ্লিকেশন গোপনীয়তা নীতি প্রো অ্যাপ ইনস্টল করুন কেন প্রো ইনস্টল ক্লাউড সিঙ্ক চালানোর জন্য ব্যাপক সার্ভারের খরচ সাপোর্ট ডেভেলপার, \n\n প্রথমে সর্বশেষ বৈশিষ্ট্যগুলি পান \n\n কিছু অতিরিক্ত বৈশিষ্ট্যগুলি শুধুমাত্র প্রো ব্যবহারকারীদের জন্য উপলব্ধ হবে - কেন মেঘ সমন্বয় সক্ষম - ডিভাইসের পরিবর্তনগুলির বিরুদ্ধে আপলোড এবং ব্যাকআপ করুন \n\n একাধিক ডিভাইসের মধ্যে সমন্বয় করুন, এবং দ্রুত আপডেট পান করুন \n\n Google Firebase সার্ভারে নিরাপদে সংরক্ষিত - হ্যালো, - উপাদান টীকা এখন স্কারলেট! - এবার শুরু করা যাক কপি @@ -257,30 +202,24 @@ আমি বুঝেছি স্বয়ংক্রিয়ভাবে রপ্তানি করুন ব্যাকআপ হিসাবে বহিরাগত ফাইল ঘন ঘন রপ্তানি নোট - ভাবমূর্তি ইমেজ যোগ বা পরিবর্তন করতে ক্লিক করুন শুধুমাত্র একবার দৈনন্দিন - প্রথা - বিটা অনুস্মারক এবং এলার্ম %s ওপেন সোর্স এবং কেউ এটি আরও ভালো করার জন্য অবদান রাখার জন্য উন্মুক্ত। এটি বর্তমানে %s দ্বারা নির্মিত এবং রক্ষণাবেক্ষণ করা হয়। %s একটি সহজ নোট গ্রহণের অ্যাপ্লিকেশন। এটি ব্যবহার করা খুব কঠিন অভিজ্ঞতা ছাড়া দ্রুত রিচ টেক্সট ইনপুট অনুমতি দেয়। এটি বহু টাস্কিং একটি বাতাস তোলে। হাই, আমরা ডিজাইনার এবং প্রোগ্রামারের একটি জোড়া, যিনি %s তৈরি করেছেন। আমরা সুন্দর, সাবধানে ডিজাইন ফ্রি, বিজ্ঞাপন-মুক্ত বা ন্যূনতম বিজ্ঞাপন অ্যাপ্লিকেশন তৈরি করতে চেষ্টা করি যা সকলের জন্য দুর্দান্ত ইউটিলিটি প্রদান করে! %d হোম স্ক্রিনে লাইন - অনুসন্ধান %s… বিতর্ক মুক্ত - বিভাজক অ্যাপ থিম থিম জন্য পটভূমি রঙ নির্বাচন করুন অ্যাপ থিম নির্বাচন করুন ট্যাগ যোগ বা পরিবর্তন করতে ক্লিক করুন - আরো কর্ম দেখান নোট নির্বাচন করুন নির্বাচন করুন এবং একযোগে একাধিক নোট কর্ম সঞ্চালন নোট ট্র্যাশে সরানো হয় @@ -320,8 +259,6 @@ নোট মার্জ করুন মার্কডাউন হিসাবে রপ্তানি করুন মার্কডাউন বিন্যাসে নোট রপ্তানি করুন। (আপনি এই অ্যাপ্লিকেশন ফিরে এই আমদানি করতে পারবেন না) - ফোল্ডার রপ্তানি করুন - সব রপ্তানি সংরক্ষিত হয় যেখানে চয়ন করুন পটভূমি জন্য থিম রঙ ব্যবহার করুন পটভূমি জন্য নোট রঙ ব্যবহার করুন সক্ষম করা diff --git a/base/src/main/res/values-de/strings.xml b/base/src/main/res/values-de/strings.xml index dbfacddc..ad2483f8 100644 --- a/base/src/main/res/values-de/strings.xml +++ b/base/src/main/res/values-de/strings.xml @@ -6,17 +6,13 @@ Teilen über… Keine Notizen Es scheint, dass Sie keine Notizen hinzugefügt haben. Klicken Sie, um eine Notiz hinzuzufügen. - Notiz hinzufügen… - Schnellnotiz hinzufügen… Bearbeiten Im Popup öffnen - Notiz löschen Kopieren Notiz senden… Aktion wählen… Inhalt hinzufügen… Überschrift hinzufügen… - Unterüberschrift hinzufügen… Zitat hinzufügen… Artikel hinzufügen… Code hinzufügen … @@ -34,7 +30,6 @@ Geben Sie eine neue PIN ein Geben Sie zum Entsperren die PIN ein Geben Sie die aktuelle PIN ein - Code eingeben Überprüfen Entsperren Einstellen @@ -45,9 +40,6 @@ Notizen in einem gestaffelten Raster anzeigen Markdown-Unterstützung Erlaube Markdown-Formatierung - Beispiele - Markdown-Einstellungen - Sehen Sie, wie man Markdown in Notizen verwendet Optionen und Einstellungen Notizen exportieren @@ -69,22 +61,14 @@ In Datei exportieren… Erledigt Teilen - Über uns und mehr Im Play Store bewerten Beitragen App-Version Über die App - Tagmodus Farbe ändern - Tippen Sie hier, um die Notiz im Tagmodus anzuzeigen Tippen, um die Hintergrundfarbe der Notiz zu ändern - Nachtmodus aktivieren - Aktiviere dunkles Design als Standard - Tagmodus aktivieren - Tagmodus als Standard aktivieren - Nachtmodus Dauerhaft löschen Notiz dearchivieren Notiz archivieren @@ -92,7 +76,6 @@ Als Favorit markieren Notiz wiederherstellen Ab in den Müll - Tippen, um die Notiz im Nachtmodus anzuzeigen. Zum Öffnen der Notiz antippen Tippen, um den Notizinhalt in die Zwischenablage zu kopieren Tippen, um Notizinhalte an andere Apps weiterzugeben @@ -104,21 +87,13 @@ Tippen, um die Markierung als Favorit zu entfernen Zum Archivieren der Notiz antippen Tippen Sie auf, um die Notiz zu dearchivieren - Deine Notizen… Hauptseite - Alle Ihre normalen und Lieblingsnotizen Favoriten - Alle deine Lieblingsnotizen Archiviert - Alle archivierten Notizen Papierkorb - Alle Ihre Notizen im Papierkorb Benachrichtigung erstellen Hinweis Benachrichtigungen Verschlossen - Alle Ihre gesperrten Notizen - Stichworte - Alle Ihre Notiz-Tags Neuen Tag hinzufügen … Tag bearbeiten … Tag eingeben @@ -129,8 +104,6 @@ Einführen Abschrift in der Notizliste Aktivieren Sie Markdown-Formatierung in der Liste der Notizen - Umfrage ausfüllen - Hilf uns, die App zu verbessern, indem du uns sagst, was dir gefällt Tags ändern @@ -139,8 +112,6 @@ Erfahren Sie mehr über das Open-Source-Projekt Bibliotheken Open-Source-Projekt - Über das Open-Source-Projekt - Hinweis Erinnerung Erinnerungsdatum Erinnerungszeit Häufigkeit wiederholen @@ -155,8 +126,6 @@ Schnittstelle und Erfahrung Wählen Sie, wie die App aussieht und sich anfühlt. - Hinweis Einstellungen - Wählen Sie Notiz und andere Einstellungen. Über Erfahren Sie mehr über uns und die App. Standard Hinweisfarbe @@ -191,19 +160,6 @@ Notiz hinzufügen Prüfliste hinzufügen - Blockformatierung - Überschrift - Unter Überschrift - Text - Zitat - Code - Checkliste - Abzeichnungsformatierung - Fett gedruckt - Kursivschrift - Unterstreichen - Streik - Liste Wählen Sie eine Notiz Wählen Sie Notizen die Einstellungen @@ -216,29 +172,18 @@ Was gibt\'s Neues Erfahren Sie, was in den letzten Updates der App neu ist Übersetzen - Klicken Sie auf, um eine neue Notiz hinzuzufügen - Klicken Sie hier, um die Startseite anzuzeigen - Das Home-Menü enthält Ihre Favoriten- und Archivnotizen sowie die Anwendungseinstellungen. Sie können Ihre Tags auch hier finden. In der App anmelden Melden Sie sich für Cloud-Backup und -Synchronisierung an Ausloggen Melden Sie sich ab, um die Cloud-Sicherung zu stoppen - Anmeldung mit Google - Anmelden … - Du bist eingeloggt Google-Anmeldung fehlgeschlagen Datenschutz-Bestimmungen App-Datenschutzrichtlinie für den Inhalt Installieren Sie die Pro App Warum Pro installieren? Unterstütze den Entwickler für die enormen Serverkosten, um die Cloud-Synchronisierung auszuführen \n\n Hol dir die neuesten Funktionen zuerst \n\n Einige zusätzliche Funktionen sind nur für Pro-Benutzer verfügbar - Warum sollte Cloud Sync aktiviert sein? - Hochladen und Sichern gegen Geräteänderungen \n\n Synchronisieren Sie mehrere Geräte und erhalten Sie schnelle Updates \n\n Sicher auf den Google Firebase Servern gespeichert - Hallo, - Material Notes ist jetzt Scarlet! - Loslegen Kopieren @@ -255,30 +200,24 @@ Ich verstehe Automatisch exportieren Exportiert häufig Notizen in eine externe Datei als Backup - Bild Klicken Sie hier, um ein Bild hinzuzufügen oder zu ändern Nur einmal Täglich - Brauch - Beta Erinnerungen und Alarme %s ist Open Source und jeder ist offen, um dazu beizutragen, dass es besser wird. Es wird derzeit von %s gebaut und gewartet. %s ist eine einfache Notiz App. Es ermöglicht eine schnelle Rich-Text-Eingabe, ohne dass die Bedienung sehr schwierig wird. Es macht Multitasking zum Kinderspiel. Hallo, wir sind ein Paar Designer und Programmierer, die %s erstellt haben. Wir sind bestrebt, schöne, sorgfältig gestaltete kostenlose, werbefreie oder minimale Anzeigen-Apps zu entwickeln, die allen Nutzern großen Nutzen bieten! %d Zeilen auf dem Startbildschirm - Suche %s … Ablenkung frei - Separator App-Thema Wählen Sie die Hintergrundfarbe für das Thema Wählen Sie App Theme Klicken Sie hier, um Tags hinzuzufügen oder zu ändern - Mehr Aktionen anzeigen Wählen Sie Notizen aus Wählen Sie mehrere Notizen gleichzeitig aus und führen Sie sie aus Die Notiz wurde in den Papierkorb verschoben @@ -318,8 +257,6 @@ Notizen zusammenführen Als Abschrift exportieren Notizen im Abschriftenformat exportieren. (Sie können diese nicht zurück in die App importieren.) - Ordner exportieren - Wählen Sie, wo alle Exporte gespeichert werden Verwenden Sie die Themenfarbe für den Hintergrund Verwenden Sie die Notenfarbe für den Hintergrund Aktivieren diff --git a/base/src/main/res/values-es/strings.xml b/base/src/main/res/values-es/strings.xml index 751302a1..0545ac0d 100644 --- a/base/src/main/res/values-es/strings.xml +++ b/base/src/main/res/values-es/strings.xml @@ -6,17 +6,13 @@ Compartir usando … No Notas Parece que no has añadido ninguna nota. Haga clic para agregar una nota. - Agrega una nota… - Añadir una nota rápida … Editar nota Abrir en Popup - Eliminar nota Copiar nota Enviar nota Elige Acción… Agregar contenido… Añadir encabezado … - Add Sub Heading … Añadir texto citado … Añadir artículo… Añadir código … @@ -26,9 +22,6 @@ Mostrar notas en una grilla escalonada Apoyo Markdown Permitir formato compatible con markdown - Ejemplos - Configuración de reducción - Vea cómo usar el descuento en notas Nota de bloqueo Desbloquear nota @@ -44,7 +37,6 @@ Ingrese un nuevo PIN Ingresa el PIN para desbloquear Ingrese el PIN actual - introduzca el código Verificar desbloquear Conjunto @@ -69,22 +61,14 @@ Exportar a archivo … Hecho Compartir - Sobre nosotros y más Valorar en Play Store Contribuir Version de aplicacion Acerca de la aplicación - Modo día Notas de búsqueda … Cambiar el color - Toca para ver la nota en modo día Toca para cambiar el color de fondo de la nota - Habilitar modo nocturno - Habilitar el tema oscuro como predeterminado - Habilitar modo día - Habilitar el tema claro como predeterminado - Modo nocturno borrar permanentemente Nota de desarchivo Nota de archivo @@ -92,7 +76,6 @@ Marcar favorito Restaurar nota Mover a la papelera - Toca para ver la nota en modo nocturno Toca para abrir una nota para editar Toca para copiar el contenido de la nota en el portapapeles Toca para compartir contenido de notas en otras aplicaciones @@ -104,21 +87,13 @@ Toca para marcar la nota no como favorita Toca para archivar la nota Toca para desarchivar la nota - Sus notas … Casa - Todas sus notas normales y favoritas Favoritos - Todas tus notas favoritas Archivado - Todas tus notas archivadas Basura - Todas tus notas en la basura Crear notificación Nota Notificaciones Bloqueado - Todas tus notas bloqueadas - Etiquetas - Todas las etiquetas de tus notas Agregar nueva etiqueta … Editar etiqueta … enter tag @@ -129,8 +104,6 @@ Importar Retraso en la lista de notas Habilitar el formateo Markdown en la lista de notas - Completar encuesta - Ayúdenos a mejorar la aplicación diciéndonos lo que le gusta Cambiar etiquetas @@ -139,8 +112,6 @@ Sepa más sobre el proyecto de código abierto Bibliotecas Proyecto de código abierto - Acerca del Proyecto de Código Abierto - Recordatorio de nota Fecha de recordatorio Tiempo de Recordatorio Repetir frecuencia @@ -155,8 +126,6 @@ Interfaz y experiencia Elija cómo se ve y se siente la aplicación. - Preferencias de notas - Elija nota y otras configuraciones. Acerca de Conozca más sobre nosotros y la aplicación. Color predeterminado de la nota @@ -191,19 +160,6 @@ Añadir la nota Agregar lista de verificación - Formateo de bloque - Título - Sub encabezado - Texto - Citar - Código - Checklist - Formato de marcado - Negrita - Cursiva - Subrayar - Huelga - Lista Seleccione una nota Seleccionar notas Configuraciones @@ -217,29 +173,18 @@ Qué hay de nuevo Saber qué hay de nuevo en las actualizaciones recientes de la aplicación Traducir - Haga clic para agregar una nueva nota - Haga clic para ver las opciones de inicio - El menú de inicio tiene sus notas favoritas y de archivo, así como la configuración de la aplicación. Usted puede encontrar sus etiquetas aquí también. Iniciar sesión en la aplicación Iniciar sesión para copia de seguridad y sincronización en la nube Desconectar Cerrar sesión para detener la copia de seguridad en la nube - Inicia sesión con Google - Firmando … - Usted ha iniciado sesión Error de inicio de sesión de Google Política de privacidad Política de privacidad de la aplicación para el contenido Instalar la aplicación Pro Por qué instalar Pro Soporte al desarrollador para los costos masivos del servidor, para ejecutar la sincronización en la nube \n\n Obtenga las características más recientes primero \n\n Algunas características adicionales estarán disponibles solo para usuarios Pro - Por qué habilitar Cloud Sync - Cargar y realizar copias de seguridad de los cambios en el dispositivo \n\n Sincronizar entre varios dispositivos y obtener actualizaciones rápidas \n\n Guardado de forma segura en los servidores de Google Firebase - Hola, - Material Notes ahora es Scarlet! - Empezar Dupdo @@ -256,30 +201,24 @@ Entiendo Exportar Automáticamente Exportar notas con frecuencia a un archivo externo como respaldo - Imagen Haga clic para agregar o cambiar la imagen Sólo una vez Diario - Personalizado - Beta Recordatorios y alarmas %s es de código abierto y cualquiera está dispuesto a contribuir para mejorarlo. Actualmente está construido y mantenido por %s. %s es una aplicación para tomar notas. Permite la entrada rápida de texto enriquecido sin hacer que la experiencia sea muy difícil de usar. Hace multitarea en un abrir y cerrar de ojos. Hola, somos un par de diseñador y programador, que creó %s. ¡Nos esforzamos por crear aplicaciones publicitarias hermosas, cuidadosamente diseñadas, gratuitas, sin publicidad o mínimas que sean de gran utilidad para todos! %d líneas en la pantalla de inicio - Buscar %s… Distracción gratis - Separador Tema de la aplicación Seleccione el color de fondo para el tema Seleccione el tema de la aplicación Haga clic para agregar o cambiar etiquetas - Mostrar más acciones Seleccione notas Selecciona y realiza acciones en múltiples notas a la vez. La nota fue movida a la basura. @@ -320,8 +259,6 @@ Notas de fusión Exportar como Markdown Exportar notas en formato markdown. (No puede importar estos de nuevo a la aplicación) - Carpeta de exportación - Elija donde se almacenan todas las exportaciones Utilice el color del tema para el fondo Usa el color de la nota para el fondo. Habilitar diff --git a/base/src/main/res/values-fr/strings.xml b/base/src/main/res/values-fr/strings.xml index 5646ec86..25a6590e 100644 --- a/base/src/main/res/values-fr/strings.xml +++ b/base/src/main/res/values-fr/strings.xml @@ -6,17 +6,13 @@ Partager avec … Pas de notes Il semble que vous n\'ayez pas ajouté de notes. Cliquez pour ajouter une note. - Ajouter une note… - Ajouter une note rapide … Modifier Ouvrir comme pop-up - Supprimer Copier Partager Choisir une action… Ajouter du contenu… Ajouter un titre … - Ajouter un sous-titre … Ajouter une citation … Ajouter un item… Ajouter du code … @@ -34,7 +30,6 @@ Entrer le nouveau code PIN Entrer le code PIN pour déverrouiller Entrer le code PIN actuel - Entrer le code Vérifier Ouvrir Activer @@ -45,9 +40,6 @@ Afficher les notes dans une grille décalée Prise en charge du formatage Autoriser le formatage pris en charge - Exemples - Paramètres de formatage - Voir comment utiliser le formatage dans les notes Options et paramètres Exporter des notes Exporter des notes vers le stockage de l\'appareil @@ -67,23 +59,15 @@ Exportation vers un fichier … Terminé Partager - À propos de nous et plus Noter sur le Play Store Contribuer Version de l\'application À propos de l\'application Rechercher des notes … - Mode jour Changer de couleur - Appuyer pour afficher la note en mode jour Appuyer pour modifier la couleur d\'arrière-plan de la note - Activer le mode nuit - Activer le thème sombre par défaut - Activer le mode jour - Activer le thème lumineux par défaut - Mode nuit Supprimer définitivement Retirer des archives Archiver @@ -91,7 +75,6 @@ Marquer comme favori Restaurer Mettre à la corbeille - Appuyer pour afficher la note en mode nuit Appuyer pour éditer la note Appuyer pour copier le contenu de la note dans le presse-papiers Appuyer pour partager le contenu de la note avec d\'autres applications @@ -103,21 +86,13 @@ Appuyer pour retirer la note des favoris Appuyer pour archiver la note Appuyer pour retirer la note des archives - Vos notes … Accueil - Toutes vos notes normales et préférées Favoris - Toutes vos notes préférées Archives - Toutes vos notes archivées Corbeille - Toutes vos notes à la corbeille Créer une notification Notifications Verrouillé - Toutes vos notes verrouillées - Tags - Toutes vos tags Ajouter un nouveau tag Modifier le tag … entrer le tag @@ -128,8 +103,6 @@ Importer Markdown dans la liste des notes Activer le formatage Markdown dans la liste des notes - Remplir l\'enquête - Aidez-nous à améliorer l\'application en nous disant ce que vous aimez Modifier les tags @@ -138,8 +111,6 @@ En savoir plus sur le projet open source Bibliothèques Projet Open Source - À propos du projet Open Source - Rappel Date du rappel Heure du rappel Fréquence du rappel @@ -154,8 +125,6 @@ Interface Définir l\'apparence de l\'application. - Préférences des notes - Définir les paramètres des notes. À propos En savoir plus sur nous et sur l\'application. Couleur des notes par défaut @@ -190,19 +159,6 @@ Ajouter une note Ajouter une liste - Formatage du texte - Titre - Sous-titre - Texte - Citation - Code - Liste - Formatage Markdown - Gras - Italique - Souligner - Barrer - Liste Sélectionner une note Sélectionner des notes Paramètres @@ -216,29 +172,18 @@ Quoi de neuf Voir les nouveautés dans les mises à jour récentes de l\'application Traduire - Cliquer pour ajouter une nouvelle note - Cliquer pour voir les options de l\'accueil - Le menu de l\'accueil montre vos notes préférées et archivées, ainsi que les paramètres de l\'application. Vous pouvez trouver vos tags ici aussi. Connectez-vous à l\'application Connexion pour la sauvegarde et la synchronisation dans le cloud Déconnexion Se déconnecter pour arrêter la sauvegarde dans le cloud - Connectez-vous avec Google - Connexion … - Vous êtes authentifié Échec de la connexion à Google Politique de confidentialité Politique de confidentialité de l\'application pour le contenu Installer l\'application Pro Pourquoi installer Pro Supporter le développeur pour les coûts massifs du serveur, pour exécuter la synchronisation du cloud \n\n Récupérer les dernières fonctionnalités en premier \n\n Certaines fonctionnalités supplémentaires ne seront disponibles que pour les utilisateurs Pro - Pourquoi activer Cloud Sync - Téléchargement et sauvegarde des modifications de l\'appareil \n\n Synchronisation entre plusieurs appareils et mise à jour rapide \n\n Enregistrement sécurisé sur les serveurs Google Firebase - Bonjour, - Notes de matériaux est maintenant Scarlet! - Commencer Copie @@ -255,30 +200,24 @@ Je comprends Exporter automatiquement Exporter des notes fréquemment vers un fichier externe en tant que sauvegarde - Image Cliquez pour ajouter ou modifier l\'image Juste une fois du quotidien - Douane - Bêta Rappels et alarmes %s est open source et tout le monde est ouvert pour contribuer à l\'améliorer. Il est actuellement construit et maintenu par %s. %s est une application de prise de notes simple. Il permet une saisie rapide en texte enrichi sans rendre l\'expérience très difficile à utiliser. Cela rend le multitâche facile. Salut, nous sommes une paire de concepteur et programmeur, qui a créé %s. Nous nous efforçons de créer de superbes applications gratuites, sans annonces ou minimes qui soient utiles à tous! %d lignes sur l\'écran d\'accueil - Rechercher %s… Distraction Gratuit - Séparateur Thème de l\'application Sélectionnez la couleur de fond pour le thème Sélectionnez le thème de l\'application Cliquez pour ajouter ou modifier des tags - Afficher plus d\'actions Sélectionnez des notes Sélectionner et effectuer des actions sur plusieurs notes à la fois La note a été déplacée à la corbeille @@ -318,8 +257,6 @@ Fusionner les notes Exporter comme démarque Exporter des notes au format markdown. (Vous ne pouvez pas les importer dans l\'application) - Dossier d\'exportation - Choisissez où toutes les exportations sont stockées Utiliser la couleur du thème pour le fond Utiliser la couleur de note pour le fond Activer diff --git a/base/src/main/res/values-hi/strings.xml b/base/src/main/res/values-hi/strings.xml index 5904d7c2..c1fe17c3 100644 --- a/base/src/main/res/values-hi/strings.xml +++ b/base/src/main/res/values-hi/strings.xml @@ -6,17 +6,13 @@ का उपयोग साझा करें … नोट्स नहीं ऐसा लगता है कि आपने कोई नोट नहीं जोड़ा है नोट जोड़ने के लिए क्लिक करें - टिप्पणी जोड़ें… - त्वरित नोट जोड़ें … नोट संपादित करें पॉपअप में खोलें - नोट हटाएं प्रतिलिपि नोट नोट भेजें विकल्प चुने… सामग्री जोड़ें… शीर्षक जोड़ें … - उप शीर्षक जोड़ें … उद्धरित पाठ जोड़ें … सामान जोडें… कोड जोड़ें … @@ -34,7 +30,6 @@ नया पिन दर्ज करें अनलॉक करने के लिए पिन दर्ज करें वर्तमान पिन दर्ज करें - कोड दर्ज करें सत्यापित करें अनलॉक सेट @@ -45,9 +40,6 @@ एक कंपित ग्रिड में नोट्स दिखाएं मार्कडाउन समर्थन मार्कडाउन समर्थित स्वरूपण की अनुमति दें - उदाहरण - मार्कडाउन सेटिंग्स - नोट्स में मार्कडाउन का उपयोग कैसे करें विकल्प और सेटिंग्स निर्यात नोट्स साझा करने के लिए डिवाइस संग्रहण में नोट निर्यात करें @@ -67,23 +59,15 @@ फाइल करने के लिए निर्यात | किया हुआ शेयर - हमारे बारे में और अधिक प्ले स्टोर पर रेट करें योगदान एप्लिकेशन वेरीज़न ऐप के बारे में नोट्स खोजें … | - दिन मोड रंग बदलना - दिन मोड में नोट देखने के लिए टैप करें नोट के पृष्ठभूमि का रंग बदलने के लिए टैप करें - नाइट मोड सक्षम करें - डिफ़ॉल्ट के रूप में गहरा विषय सक्षम करें - डे मोड सक्षम करें - प्रकाश विषय को डिफ़ॉल्ट के रूप में सक्षम करें - रात्री स्वरुप स्थायी रूप से मिटाएं अनचाही नोट पुरालेख नोट @@ -91,7 +75,6 @@ मार्क पसंदीदा नोट पुनर्स्थापित करें रद्दी में डालें - रात मोड में नोट देखने के लिए टैप करें संपादन के लिए नोट खोलने के लिए टैप करें क्लिपबोर्ड पर नोट सामग्री कॉपी करने के लिए टैप करें अन्य ऐप्स पर नोट सामग्री साझा करने के लिए टैप करें @@ -103,21 +86,13 @@ नोट को पसंदीदा के रूप में चिह्नित करने के लिए टैप करें नोट संग्रह करने के लिए टैप करें ध्यान हटाने के लिए टैप करें - आपका नोट्स … | होम - आपके सभी सामान्य और पसंदीदा नोट्स पसंदीदा - आपके सभी पसंदीदा नोट्स संग्रहीत - आपके सभी संग्रहीत नोट कचरा - कचरा में आपके सभी नोट्स अधिसूचना बनाएं नोट नोटिफिकेशन बंद - आपके सभी लॉक नोट्स - टैग - आपके सभी नोट टैग नई टैग जोड़ें | | टैग करें संपादित करें | टैग दर्ज करें @@ -128,8 +103,6 @@ आयात नोट सूची में मार्कडाउन नोट्स की सूची में मार्कटाउन स्वरूपण सक्षम करें - सर्वेक्षण भरें - हमें बताएं कि आप क्या पसंद करते हैं टैग बदलें @@ -138,8 +111,6 @@ ओपन सोर्स परियोजना के बारे में अधिक जानें पुस्तकालय ओपन सोर्स प्रोजेक्ट - ओपन सोर्स प्रोजेक्ट के बारे में - नोट अनुस्मारक अनुस्मारक तिथि अनुस्मारक समय आवृत्ति दोहराएं @@ -154,8 +125,6 @@ इंटरफ़ेस और अनुभव चुनें कि ऐप कैसा दिखता है और कैसे लगता है। - नोट प्राथमिकताएं - नोट और अन्य सेटिंग्स चुनें के बारे में हमारे और ऐप के बारे में अधिक जानें डिफ़ॉल्ट नोट रंग @@ -190,19 +159,6 @@ नोट जोड़े चेकलिस्ट जोड़ें - ब्लॉक फ़ॉर्मेटिंग - शीर्षक - उप शीर्षक - टेक्स्ट - उद्धरण - कोड - चेकलिस्ट - मार्कडाउन फ़ॉर्मेटिंग - साहसिक - इटैलिक - रेखांकित करना - धरना - सूची एक नोट चुनें नोट्स का चयन करें सेटिंग्स @@ -216,29 +172,18 @@ नया क्या है एप के हालिया अपडेट में नया क्या है, पता करें अनुवाद करना - कोई नया नोट जोड़ने के लिए क्लिक करें - होम विकल्प देखने के लिए क्लिक करें - होम मेनू में आपके पसंदीदा और संग्रह नोट्स, साथ ही एप्लिकेशन सेटिंग्स हैं। आप यहां अपने टैग भी पा सकते हैं। ऐप में साइन इन करें क्लाउड बैकअप और सिंक्रनाइज़ेशन के लिए साइन इन करें साइन आउट क्लाउड बैकअप को रोकने के लिए साइन आउट करें - Google के साथ साइन इन करें - साइनिंग इन … | - आप लोग्ड इन हो चुके हैं Google लॉगिन विफल गोपनीयता नीति सामग्री के लिए ऐप गोपनीयता नीति प्रो ऐप इंस्टॉल करें क्यों प्रो स्थापित करें क्लाउड सिंक चलाने के लिए बड़े सर्वर लागतों के लिए समर्थन डेवलपर का समर्थन करें \n\n नवीनतम सुविधाओं को पहले प्राप्त करें \n\n कुछ अतिरिक्त सुविधाएं केवल प्रो उपयोगकर्ता के लिए उपलब्ध होंगी - क्लाउड सिंक क्यों सक्षम करें - डिवाइस के बदलावों के विरुद्ध अपलोड करें और बैकअप लें \n\n एकाधिक उपकरणों के बीच समन्वय करें, और त्वरित अपडेट प्राप्त करें \n\n Google Firebase सर्वर पर सुरक्षित रूप से सहेजा गया - नमस्ते, - सामग्री नोट्स अब लाल रंग है! - शुरू हो जाओ प्रतिलिपि @@ -255,30 +200,24 @@ मै समझता हुँ स्वचालित रूप से निर्यात करें बाह्य फ़ाइल में बार-बार बैकअप के रूप में निर्यात करें - छवि चित्र जोड़ने या बदलने के लिए क्लिक करें सिर्फ एक बार रोज - रिवाज - बीटा अनुस्मारक और अलार्म %s ओपन सोर्स है और कोई इसे बेहतर बनाने के लिए योगदान करने के लिए खुला है। यह वर्तमान में %s द्वारा बनाया और रखरखाव किया गया है। %s एक साधारण नोट लेने वाला ऐप है यह प्रयोग करने में बहुत मुश्किल अनुभव किए बिना त्वरित रिच टेक्स्ट इनपुट की अनुमति देता है यह बहु-कार्य करने वाला हवा बनाता है नमस्ते, हम डिजाइनर और प्रोग्रामर की एक जोड़ी हैं, जिन्होंने %s बनाया है हम सुन्दर, सावधानीपूर्वक डिज़ाइन किए गए मुक्त, विज्ञापन-मुक्त या न्यूनतम विज्ञापन एप्लिकेशन बनाने की कोशिश करते हैं जो हर किसी के लिए महान उपयोगिता प्रदान करते हैं! %d होम स्क्रीन पर लाइनें - खोज %s … व्याकुलता मुक्त - सेपरेटर ऐप थीम विषय के लिए पृष्ठभूमि रंग का चयन करें ऐप थीम का चयन करें टैग जोड़ने या बदलने के लिए क्लिक करें - अधिक क्रियाएं दिखाएं नोट्स का चयन करें एक साथ कई नोट्स पर क्रियाएं चुनें और निष्पादित करें नोट ट्रैश में ले जाया गया था @@ -318,8 +257,6 @@ नोट्स मिलाएं मार्कडाउन के रूप में निर्यात करें मार्कडाउन प्रारूप में नोट्स निर्यात करें। (आप इन एप्लिकेशन को वापस आयात नहीं कर सकते) - निर्यात फ़ोल्डर - चुनें कि सभी निर्यात कहाँ संग्रहीत हैं पृष्ठभूमि के लिए थीम रंग का उपयोग करें पृष्ठभूमि के लिए नोट रंग का उपयोग करें सक्षम करें diff --git a/base/src/main/res/values-it/strings.xml b/base/src/main/res/values-it/strings.xml index 2a98a10d..191c73a5 100644 --- a/base/src/main/res/values-it/strings.xml +++ b/base/src/main/res/values-it/strings.xml @@ -6,25 +6,18 @@ Condividi con… Nessuna nota Sembra che non hai aggiunto alcuna nota. Fai clic per aggiungere una nota. - Aggiungi una nota… - Aggiungi nota veloce… Modifica nota Apri in popup - Elimina nota Copia nota Invia nota Scegli un\'azione… Aggiungi contenuto… Aggiungi intestazione… - Aggiungi sottotitolo… Aggiungi testo citato… Aggiungi elemento… Aggiungi codice… Markdown Support Consenti la formattazione markdown - Esempi - Impostazioni Markdown - Scopri come utilizzare il markdown nelle note Opzioni e impostazioni Esporta note Esporta le note nella memoria del dispositivo @@ -44,7 +37,6 @@ Esportazione nel file… Ok Condividi - Informazioni Vota su Play Store Contribuisci Versione dell\'app @@ -64,7 +56,6 @@ Inserisci il nuovo PIN Inserisci il PIN per sbloccare Inserisci il PIN corrente - Inserisci il PIN Verifica Sblocca Imposta @@ -74,16 +65,9 @@ Abilita layout a griglia Mostra le note in una griglia sfalsata - Modalità giorno Cambia colore - Tocca per visualizzare la nota nella modalità giorno Tocca per cambiare il colore di sfondo della nota - Abilita modalità notte - Abilita il tema scuro come predefinito - Abilita modalità giorno - Abilita il tema chiaro come predefinito - Modalità notturna Elimina definitivamente Rimuovi nota dall\'archivio Archivia nota @@ -91,7 +75,6 @@ Aggiungi ai preferiti Ripristina nota Sposta nel cestino - Tocca per visualizzare la nota nella modalità notte Tocca per modificare la nota Tocca per copiare il contenuto della nota negli appunti Tocca per condividere il contenuto della nota con altre app @@ -103,21 +86,13 @@ Tocca per contrassegnare la nota come non preferita Tocca per archiviare la nota Tocca per annullare l\'archiviazione della nota - I tuoi appunti… Home - Tutte le tue note normali e preferite Preferiti - Tutte le tue note preferite Archivio - Tutte le tue note archiviate Cestino - Tutte le tue note nel cestino Crea notifica Note in notifica Protetti - Tutte le tue note protette - Tag - Tutti i tuoi tag Aggiungi nuovo tag… Modifica tag… inserisci il tag @@ -128,8 +103,6 @@ Importa Markdown in Elenco note Abilita la formattazione Markdown nell\'elenco delle note - Rispondi al sondaggio - Aiutaci a migliorare l\'app dicendoci cosa ti piace Cambia tag @@ -138,8 +111,6 @@ Scopri di più sul progetto open source Librerie Progetto Open Source - Informazioni sul progetto Open Source - Aggiungi promemoria Data del promemoria Ora del promemoria Frequenza ripetizione @@ -154,8 +125,6 @@ Interfaccia ed esperienza Scegli l\'aspetto dell\'app. - Preferenze nota - Scegli preferenze e altre impostazioni. Chi siamo Scopri di più su di noi e sull\'app. Colore nota predefinita @@ -190,19 +159,6 @@ Aggiungi nota Aggiungi lista di controllo - Formattazione - Intestazione - Sottotitolo - Testo - Citazione - Codice - Lista di controllo - Formattazione Markdown - Grassetto - Corsivo - Sottolinea - Barrato - Elenco Seleziona una nota Seleziona Note Impostazioni @@ -216,29 +172,18 @@ Cosa c\'è di nuovo Scopri cosa c\'è di nuovo nei recenti aggiornamenti dell\'app Traduci - Fai clic per aggiungere una nuova nota - Clicca per vedere le opzioni della home - Il menu principale contiene le note preferite e archiviate, nonché le impostazioni dell\'applicazione. Puoi trovare anche i tuoi tag. Accedi all\'app Accedi per il backup e la sincronizzazione del cloud Disconnessione Esci per interrompere il backup del cloud - Accedi con Google - Accesso in € | - Ti sei autenticato Accesso Google non riuscito politica sulla riservatezza Politica sulla privacy dell\'app per il contenuto Installa l\'app Pro Perché installare Pro Supporta lo sviluppatore per i massivi costi del server, per eseguire la sincronizzazione del cloud \n\n Prima le funzionalità più recenti \n\n Alcune funzioni aggiuntive saranno disponibili solo per gli utenti Pro - Perché abilitare Cloud Sync - Carica e fai il backup delle modifiche del dispositivo \n\n Sincronizza tra più dispositivi e ottieni aggiornamenti rapidi \n\n Salvataggio sicuro sui server Firebase di Google - Ciao, - Le note materiali sono ora scarlatte! - Iniziare copia @@ -255,30 +200,24 @@ Capisco Esporta automaticamente Esporta frequentemente le note in un file esterno come backup - Immagine Clicca per aggiungere o cambiare immagine Solo una volta Quotidiano - costume - Beta Promemoria e allarmi %s è open source e chiunque è aperto a contribuire a migliorarlo. Attualmente è costruito e gestito da %s. %s è una semplice app per prendere appunti. Permette di inserire rapidamente Rich Text senza rendere l\'esperienza molto difficile da usare. Rende il multi-tasking un gioco da ragazzi. Ciao, siamo una coppia di designer e programmatori, che ha creato %s. Ci sforziamo di creare app pubblicitarie gratuite, pubblicitarie o minime, progettate con cura, che siano di grande utilità per tutti! %d linee sulla schermata iniziale - Cerca %s … Distrazione gratuita - Separatore Tema dell\'app Seleziona il colore di sfondo per il tema Seleziona tema app Fai clic per aggiungere o modificare i tag - Mostra più azioni Seleziona Note Seleziona ed esegui azioni su più note contemporaneamente La nota è stata spostata nel cestino @@ -318,8 +257,6 @@ Unisci note Esporta come markdown Esporta le note in formato markdown. (Non è possibile importarli nuovamente nell\'app) - Esporta cartella - Scegli dove archiviare tutte le esportazioni Usa il colore del tema per lo sfondo Usa il colore della nota per lo sfondo Abilitare diff --git a/base/src/main/res/values-iw/strings.xml b/base/src/main/res/values-iw/strings.xml index 346d330a..ec2cbde8 100644 --- a/base/src/main/res/values-iw/strings.xml +++ b/base/src/main/res/values-iw/strings.xml @@ -6,39 +6,26 @@ שתף באמצעות … אין הערות נראה שלא הוספת הערות. לחץ כדי להוסיף הערה. - הוסף הערה… - הוסף הערה מהירה … ערוך הערה פתח ב- Popup - מחק הערה העתק הערה שלח הערה בחר פעולה… הוסף תוכן … הוסף כותרת … - הוסף כותרת משנה … הוסף טקסט מצוטט … הוסף פריט… הוסף קוד … חפש הערות - מצב יום שינוי צבע - הקש כדי להציג את ההערה במצב יום הקש כדי לשנות את צבע הרקע של ההערה - הפעל מצב לילה - אפשר עיצוב כהה כברירת מחדל - הפעל מצב יום - אפשר עיצוב אור כברירת מחדל הפעל פריסת רשימה הצג הערות בעמודה אחת הפעל פריסת רשת הצג הערות ברשת מטושטשת תמיכה Markdown אפשר עיצוב נתמך של סימון - דוגמאות - הגדרות Markdown - ראה כיצד להשתמש ב- markdown בהערות הערה נעל נעילת הערה בִּטָחוֹן @@ -53,7 +40,6 @@ הזן קוד PIN חדש הזן PIN כדי לבטל את הנעילה הזן את קוד ה- PIN הנוכחי - להזין את הקוד לְאַמֵת לבטל נעילה מַעֲרֶכֶת @@ -78,13 +64,11 @@ ייצוא ל- fileâ € בוצע לַחֲלוֹק - אודותינו ועוד שיעור על חנות Play לתרום גרסת האפליקציה על האפליקציה - מצב לילה מחק לצמיתות Unarchive הערה הערה בארכיון @@ -92,7 +76,6 @@ סמן מועדף שחזור הערה העבר לאשפה - הקש כדי להציג את ההערה במצב לילה הקש כדי לפתוח פתק לעריכה הקש כדי להעתיק תוכן הערה ללוח הקש כדי לשתף תוכן הערה ביישומים אחרים @@ -104,21 +87,13 @@ הקש כדי לסמן הערה לא כאתר מועדף הקש כדי להעביר לארכיון את ההערה הקש כדי לשחרר את ההערה מהארכיון - ההערות שלך בית - כל ההערות הרגילות והמועדפות שלך מועדפים - כל ההערות המועדפות עליך בארכיון - כל ההערות שלך בארכיון אַשׁפָּה - כל ההערות שלך באשפה צור הודעה הערה הודעות נָעוּל - כל ההערות הנעולות שלך - תגים - כל תגי ההערות שלך הוסף תג חדש ערוך Taga הזן תג @@ -129,8 +104,6 @@ יְבוּא Markdown ברשימת ההערות הפעל עיצוב Markdown ברשימת ההערות - מילוי סקר - עזור לנו לשפר את האפליקציה על ידי כך שתספר לנו מה אתה אוהב שנה תגים @@ -139,8 +112,6 @@ לדעת יותר על פרוייקט קוד פתוח ספריות פרוייקט קוד פתוח - על פרוייקט קוד פתוח - הערה תזכורת תאריך תזכורת זמן תזכורת חזור על תדר @@ -155,8 +126,6 @@ ממשק וניסיון בחר כיצד האפליקציה נראית ומרגישה. - הערה העדפות - בחר הערה והגדרות אחרות. על אודות למידע נוסף עלינו ועל האפליקציה. ברירת מחדל הערה צבע @@ -191,19 +160,6 @@ להוסיף הערה הוסף רשימת בדיקה - חסום עיצוב - כּוֹתֶרֶת - כותרת משנה - טֶקסט - ציטוט - קוד - צ\'ק ליסט - עיצוב סמן - נוֹעָז - אוֹתִיוֹת מוּטוֹת - לָשִׂים דָגֵשׁ - לְהַכּוֹת - רשימה בחר הערה בחר הערות הגדרות @@ -217,29 +173,18 @@ מה חדש דע מה חדש בעדכונים האחרונים של האפליקציה לתרגם - לחץ כדי להוסיף הערה חדשה - לחץ כדי לראות את אפשרויות הבית - בתפריט הבית יש הערות מועדפות וארכיוניות, כמו גם הגדרות יישום. תוכל למצוא את התגים שלך גם כאן. היכנס לאפליקציה היכנס לגיבוי ענן ולסינכרון התנתק צא כדי לעצור את גיבוי הענן - היכנס באמצעות Google - כניסה - אתה מחובר הכניסה של Google נכשלה מדיניות פרטיות מדיניות הפרטיות של האפליקציה עבור התוכן התקן Pro App למה להתקין Pro תמיכה מפתח עבור עלויות השרת מסיבית, כדי להפעיל את סנכרון ענן \n\n קבל את התכונות החדשות הראשון \n\n כמה תכונות נוספות יהיה זמין רק למשתמשים Pro - מדוע להפעיל את Cloud Sync - טען וגבה מפני שינויי מכשירים \n\n סנכרון בין מכשירים מרובים, וקבל עדכונים מהירים \n\n נשמר באופן מאובטח בשרתי Google Firebase - שלום, - הערות החומר הוא עכשיו סקרלט! - להתחיל עותק @@ -256,30 +201,24 @@ אני מבין ייצוא אוטומטי ייצוא הערות לעתים קרובות לקובץ חיצוני כגיבוי - תמונה לחץ כדי להוסיף או לשנות את התמונה רק פעם אחת יום יומי - המותאם אישית - בטא תזכורות ו אזעקות %s הוא קוד פתוח וכל אחד פתוח לתרום כדי לעשות את זה טוב יותר. הוא נבנה כעת ומתוחזק על ידי %s. %s הוא פתק פשוט לוקח יישום. זה מאפשר קלט טקסט עשיר עשיר מבלי להפוך את החוויה קשה מאוד לשימוש. זה עושה multi-tasking משב רוח. היי, אנחנו זוג מעצב ומתכנת, שיצר %s. אנו משתדלים לבנות יפה, תוכנן בקפידה ללא תשלום, מודעות חינם או מודעות מינימלי אשר משרתים כלי נהדר לכולם! %d שורות במסך הבית - חיפוש %s … הסחת דעת חינם - מפריד נושא בחר את צבע הרקע עבור ערכת הנושא בחר ערכת נושא של אפליקציות לחץ כדי להוסיף או לשנות תגים - הצג פעולות נוספות בחר הערות בחר וביצע פעולות במספר הערות בבת אחת הפתק הועבר לאשפה @@ -319,8 +258,6 @@ מיזוג הערות ייצא כסימון ייצא הערות בפורמט סימון. (אינך יכול לייבא אותם בחזרה לאפליקציה) - ייצא תיקיה - בחר היכן יאוחסן כל היצוא השתמש בצבע הנושא לרקע השתמש בצבע הערה לרקע אפשר diff --git a/base/src/main/res/values-ja/strings.xml b/base/src/main/res/values-ja/strings.xml index 908b0ded..c680877c 100644 --- a/base/src/main/res/values-ja/strings.xml +++ b/base/src/main/res/values-ja/strings.xml @@ -8,16 +8,11 @@ 共有を使用して| | ノートなし メモを追加していないようです。メモを追加する場合にクリックします。 - ノートを追加する| - 簡単なメモを追加する| メモを追加 チェックリストを追加 - ナイトモード - デイモード メモを編集する 通知の作成 ポップアップで開く - メモを削除 永久に削除 コピーノート ノートを送る @@ -37,8 +32,6 @@ ピンノート ノートを固定解除する アクションを選択する| - タップしてナイトモードでノートを表示する - タップしてメモを日中のモードで表示する タップして編集用のメモを開きます ノートのコンテンツをクリップボードにコピーするにはタップしてください タップしてメモコンテンツを他のアプリと共有する @@ -51,54 +44,24 @@ タップしてメモをアーカイブする タップしてメモの背景色を変更する メモを保存するにはタップしてください - あなたのメモやその他| ホーム - あなたの普通のノートとお気に入りのノート お気に入り - お気に入りのメモすべて アーカイブ済み - すべてのアーカイブされたメモ ロックされた - すべてのロックされたメモ - タグ - すべてのノートタグ ごみ - ごみの中のあなたのメモ コンテンツを追加する| 見出しを追加する| - サブ見出しを追加する| 引用符で囲まれたテキストを追加する| アイテムを追加する| コードを追加する| - ブロック書式設定 - 見出し - 副見出し - テキスト - 見積もり - コード - チェックリスト - マークダウンの書式設定 - 大胆な - イタリック体 - アンダーライン - ストライク - リスト オプションと設定 インターフェースと経験 アプリの見た目と感じ方を選択します。 - ノート設定 - メモやその他の設定を選択します。 私たちとアプリについてもっと知ってください。 セキュリティ ロックノートとセキュリティオプション - ナイトモードを有効にする - 暗いテーマをデフォルトとして有効にする - デイモードを有効にする - デフォルトとしてライトテーマを有効にする - マークダウン設定 - メモにmarkdownを使用する方法を参照してください リストレイアウトを有効にする ノートを1つの列に表示する グリッドレイアウトを有効にする @@ -116,8 +79,6 @@ オープンソースプロジェクトの詳細を知る 評価と批評 あなたがアプリをどれだけ好きだったか教えてください - 調査を記入する - あなたの好きなことを教えてアプリを改善してください セキュリティオプション パスコード ノートをロックする4桁のPINを設定する @@ -128,7 +89,6 @@ 新しいPINを入力してください PINを入力してロックを解除する 現在のPINを入力 - コードを入力する 確認 ロックを解除する セット @@ -141,7 +101,6 @@ マークダウンがサポートされているフォーマットを許可する ノートリストのマークダウン ノートのリストでマークダウンの書式設定を有効にする - 許可を与える インポートとエクスポートにはストレージ権限が必要です。要請された場合は許可を与えてください。 許可する @@ -152,15 +111,12 @@ ファイルにエクスポートする| 完了 シェア - 私たちについて Playストアでのレート 寄稿 アプリのバージョン アプリについて 図書館 オープンソースプロジェクト - オープンソースプロジェクトについて - メモの通知 リマインダの日付 リマインダー時間 繰り返し周波数 @@ -181,9 +137,6 @@ 新着情報 最近のアプリのアップデートで何が新しくなったのかを知る 翻訳 - クリックして新しいメモを追加する - クリックしてホームオプションを表示する - ホームメニューには、お気に入りのメモやアーカイブのメモ、アプリケーションの設定があります。ここでもあなたのタグを見つけることができます。 メモフォントサイズ メモページのテキストのフォントサイズを調整します。このプレビューでどのように見えるかを見ることができます。 ノートビューア内のテキストの%dサイズ @@ -205,20 +158,12 @@ クラウドバックアップと同期のためのサインイン サインアウト ログアウトしてクラウドバックアップを停止する - Googleでログイン - サインイン| - あなたはログインしています Googleログインに失敗しました 個人情報保護方針 コンテンツのアプリプライバシーポリシー プロアプリのインストール プロをインストールする理由 クラウド同期を実行するために大規模なサーバーコストの開発者をサポートする \n\n 最新の機能を最初に入手する \n\n いくつかの追加機能はProユーザーだけが利用できます - クラウド同期を有効にする理由 - †"デバイスの変更に対するアップロードとバックアップ \n\n 複数のデバイス間の同期とクイックアップデートの取得 \n\n Google Firebaseサーバーへの安全な保存 - こんにちは、 - マテリアルノートはスカーレットになりました! - 開始する コピー @@ -235,30 +180,24 @@ わかりました 自動的にエクスポート メモを頻繁にバックアップとして外部ファイルにエクスポートする - 画像 クリックして画像を追加または変更する 1回だけ 毎日 - カスタム - ベータ リマインダーとアラーム %s はオープンソースで誰でもオープンになっています。 現在、%sによって構築され維持されています。 %sはアプリを撮るシンプルなメモです。 体験を非常に難しくすることなく、リッチテキスト入力を素早く行うことができます。 それはマルチタスクを簡単にします。 こんにちは、私たちは%sを創造したデザイナーとプログラマーのペアです。 私たちは美しくて慎重に設計された無償、無償、または最小限の広告アプリケーションを構築して、誰にとっても大きな有用性を提供するよう努めています。 ホーム画面の%d行 - 検索%s … 気を散らさない - セパレータ アプリのテーマ テーマの背景色を選択する アプリケーションテーマを選択 クリックしてタグを追加または変更する - その他のアクションを表示 ノートを選択 一度に複数のノートでアクションを選択して実行する メモはゴミ箱に移動されました @@ -298,8 +237,6 @@ メモをマージ 値下げとしてエクスポート ノートをマークダウン形式でエクスポートします。 (これらをアプリにインポートすることはできません) - エクスポートフォルダ - すべての輸出が保管されている場所を選択してください 背景色にテーマカラーを使用 背景にメモの色を使用 有効にする diff --git a/base/src/main/res/values-ko/strings.xml b/base/src/main/res/values-ko/strings.xml index 09b64f42..d751e06b 100644 --- a/base/src/main/res/values-ko/strings.xml +++ b/base/src/main/res/values-ko/strings.xml @@ -8,16 +8,11 @@ 사용하여 공유 | 메모 없음 메모를 추가하지 않은 것 같습니다. 메모를 추가하려면 클릭하십시오. - 메모 추가하기 | - 빠른 메모 추가하기 | 메모를 추가 체크리스트 추가 - 야간 모드 - 주간 모드 메모 편집 알림 만들기 팝업에서 열기 - 메모 삭제 영구적으로 삭제 메모 복사 메모 보내기 @@ -37,8 +32,6 @@ 핀 메모 메모 고정 해제 액션 선택 | - 밤 모드에서 메모를 보려면 탭하세요. - 요일 모드에서 메모를 보려면 살짝 누르십시오. 편집하기 위해 메모를 열려면 살짝 누르십시오. 메모 내용을 클립 보드에 복사하려면 누르십시오. 메모 콘텐츠를 다른 앱과 공유하려면 탭하세요. @@ -51,54 +44,24 @@ 메모를 보관하려면 탭하세요. 메모의 배경색을 변경하려면 누르십시오. 메모를 보관 취소하려면 탭하세요. - 귀하의 메모 및 기타 … | - 당신의 평소와 좋아하는 모든 메모 즐겨 찾기 - 좋아하는 모든 메모 보관 됨 - 보관 처리 된 모든 메모 잠김 - 모든 잠긴 메모 - 태그 - 모든 메모 태그 폐물 - 휴지통에있는 모든 메모 컨텐츠 추가 | 헤딩 추가하기 | - 하위 제목 추가 … | 따옴표 붙은 텍스트 추가하기 | 항목 추가하기 | 코드 추가 | - 블록 서식 지정 - 표제 - 하위 제목 - 본문 - 인용문 - 암호 - 체크리스트 - 마크 다운 형식 - 대담한 - 이탤릭체 - 밑줄 - 스트라이크 - 명부 옵션 및 설정 인터페이스와 경험 앱의 모양과 느낌을 선택합니다. - 메모 환경 설정 - 메모 및 기타 설정을 선택하십시오. 우리와 앱에 대해 더 많이 알기. 보안 잠금 메모 및 보안 옵션 - 야간 모드 사용 - 어두운 테마를 기본값으로 사용 - 주간 모드 사용 - 조명 테마를 기본값으로 사용 - 마크 다운 설정 - 메모에서 마크 다운 사용 방법보기 목록 레이아웃 사용 단일 열에 노트 표시 그리드 레이아웃 활성화 @@ -116,8 +79,6 @@ 오픈 소스 프로젝트에 대해 더 많이 알기 별점과 리뷰 앱을 얼마나 좋아하는지 알려주세요. - 설문 조사 작성 - 내가 좋아하는 것을 말하면서 앱을 개선 할 수 있도록 도와주세요. 보안 옵션 패스 코드 메모를 잠 그려면 4 자리 PIN을 설정하십시오. @@ -128,7 +89,6 @@ 새 PIN 입력 잠금 해제 할 PIN 입력 현재 PIN 입력 - 코드를 입력 검증 터놓다 세트 @@ -141,7 +101,6 @@ 마크 다운 지원 서식 지정 허용 노트 목록의 마크 다운 메모 목록에서 마크 다운 서식 사용 - 예제들 권한 부여 가져 오기 및 내보내기에는 저장 영역 권한이 필요합니다. 요청시 허가를하십시오. 허용 @@ -152,15 +111,12 @@ 파일로 내보내기 … | 끝난 - 회사 소개 Play 스토어에서 평가하기 기부 앱 버전 앱 정보 도서관 오픈 소스 프로젝트 - 오픈 소스 프로젝트 정보 - 메모 알림 알림 날짜 미리 알림 시간 반복 주파수 @@ -181,9 +137,6 @@ What \'s New 앱의 최근 업데이트에서 새로운 점을 파악합니다. 옮기다 - 새 메모를 추가하려면 클릭하십시오. - 홈 옵션을 보려면 클릭하십시오. - 홈 메뉴에는 응용 프로그램 설정뿐만 아니라 좋아하는 노트와 아카이브 노트가 있습니다. 여기에서도 태그를 찾을 수 있습니다. 참고 글꼴 크기 메모 페이지에서 텍스트의 글꼴 크기를 조정하십시오. 미리보기에서 어떻게 보이는지 알 수 있습니다. 노트 뷰어의 텍스트 크기 %d @@ -205,20 +158,12 @@ 클라우드 백업 및 동기화를위한 로그인 로그 아웃 클라우드 백업을 중지하려면 로그 아웃하십시오. - Google로 로그인 - 로그인 | - 로그인하셨습니다. Google 로그인 실패 개인 정보 정책 콘텐츠에 대한 앱 개인 정보 취급 방침 프로 앱 설치 Pro를 설치해야하는 이유 대규모의 서버 비용, 클라우드 동기화 실행을위한 개발자 지원 \n\n 최신 기능 먼저 가져 오기 \n\n 일부 추가 기능은 Pro 사용자 만 사용할 수 있습니다 - 클라우드 동기화를 사용 설정해야하는 이유 - 장치 변경에 대한 업로드 및 백업 \n\n 여러 장치 간 동기화 및 빠른 업데이트 받기 \n\n Google Firebase 서버에 안전하게 저장 - 여보세요, - 소재 노트는 스칼렛입니다! - 시작하다 @@ -235,30 +180,24 @@ 이해 했어 자동으로 내보내기 메모를 외부 파일로 자주 내보내 백업으로 보내기 - 영상 이미지를 추가하거나 변경하려면 클릭하십시오. 한 번만 매일 - 관습 - 베타 미리 알림 및 경보 %s는 오픈 소스이며 누구나 오픈 소스를 통해 더 잘 만들 수 있습니다. 그것은 현재 %s에 의해 만들어지고 관리되고 있습니다. %s는 앱을 복용하는 간단한 메모입니다. 경험을 사용하기가 매우 어렵지 않으면 서 빠른 서식있는 텍스트 입력이 가능합니다. 그것은 멀티 태스킹 바람을 만듭니다. 안녕하세요, 우리는 %s 를 만든 디자이너와 프로그래머입니다. 우리는 아름답고 신중하게 디자인 된 무료, 광고없는 또는 최소한의 광고 응용 프로그램을 만들어 모든 사람에게 유용한 유틸리티를 제공하기 위해 노력합니다. 홈 스크린의 %d 라인 - %s 검색 … 산만 무료 - 분리 기호 앱 테마 테마의 배경색 선택 앱 테마 선택 태그를 추가하거나 변경하려면 클릭하십시오. - 추가 작업 표시 메모 선택 한 번에 여러 음표에 대한 작업 선택 및 수행 메모가 휴지통으로 이동되었습니다. @@ -298,8 +237,6 @@ 메모 병합 마크 다운으로 내보내기 메모를 축소 형으로 내 보냅니다. (다시 앱으로 가져올 수는 없습니다.) - 폴더 내보내기 - 모든 내보내기가 저장되는 위치 선택 배경에 테마 색상 사용 배경에 노트 색상 사용 사용 diff --git a/base/src/main/res/values-mr/strings.xml b/base/src/main/res/values-mr/strings.xml index ce7f00bb..004b9beb 100644 --- a/base/src/main/res/values-mr/strings.xml +++ b/base/src/main/res/values-mr/strings.xml @@ -6,17 +6,13 @@ वापरून सामायिक करा … कोणत्याही टिपा नाहीत आपण कोणत्याही नोट्स जोडले नाहीत असे दिसते एक टीप जोडण्यासाठी क्लिक करा - एक टीप जोडा … - एक द्रुत टीप जोडा … टीप संपादित करा पॉपअपमध्ये उघडा - टीप हटवा कॉपी टीप टीप पाठवा क्रिया निवडा … सामग्री जोडा … मथळा जोडा … - उप शीर्षलेख जोडा … कोट केलेला मजकूर जोडा … आयटम जोडा … कोड जोडा … @@ -43,7 +39,6 @@ फाईलवर निर्यात करत आहे … | झाले सामायिक करा - आमच्याबद्दल आणि बरेच काही प्ले स्टोअरवर रेट करा योगदान द्या अॅप आवृत्ती @@ -53,9 +48,6 @@ टीप अनलॉक करा चिन्हांकित आधार मार्कडाउन समर्थित स्वरूपण ला अनुमती द्या - उदाहरणे - चिन्हांकित करा सेटिंग्ज - नोट्समध्ये चिन्हांकित कसे वापरावे ते पहा सुरक्षा लॉकिंग नोट्स आणि सुरक्षा पर्याय सुरक्षा पर्याय @@ -68,21 +60,13 @@ नवीन पिन प्रविष्ट करा अनलॉक करण्यासाठी पिन प्रविष्ट करा वर्तमान PIN प्रविष्ट करा - कोड टाका सत्यापित करा अनलॉक करा सेट करा काढा - दिवस मोड रंग बदला - दिवस मोडमध्ये टीप पाहण्यासाठी टॅप करा टीपचा पार्श्वभूमी रंग बदलण्यासाठी टॅप करा - रात्री मोड सक्षम करा - डीफॉल्ट म्हणून गडद थीम सक्षम करा - दिवस मोड सक्षम करा - मुलभूत म्हणून प्रकाश थीम सक्षम करा - रात्र मोड कायमचे हटवा टीप संग्रहण रद्द करा संग्रहण टीप @@ -90,7 +74,6 @@ मार्क पसंतीचा टीप पुनर्संचयित करा कचरा मध्ये हलवा - रात्रीच्या मोडमध्ये टीप पाहण्यासाठी टॅप करा संपादनासाठी टिप उघडण्यासाठी टॅप करा टीप सामग्री क्लिपबोर्डवर कॉपी करण्यासाठी टॅप करा अन्य अॅप्सवर टीप सामग्री सामायिक करण्यासाठी टॅप करा @@ -102,21 +85,13 @@ टीप आवडलेली म्हणून चिन्हांकित करण्यासाठी टॅप करा टीप संग्रहित करण्यासाठी टॅप करा टीप संग्रहित करणे अनइझम करण्यासाठी टॅप करा - आपले नोट्स … | घर - आपल्या सर्व सामान्य आणि आवडत्या नोट्स आवडते - आपल्या सर्व आवडत्या टिपा संग्रहित - आपल्या सर्व संग्रहित टिपा कचरा - कचरा मधील आपल्या सर्व नोट्स सूचना तयार करा टीप सूचना लॉक केलेले - आपली सर्व लॉक केलेली टिपा - टॅग्ज - आपले सर्व नोट टॅग्ज नवीन टॅग जोडा | टॅगचे संपादन करा | टॅग प्रविष्ट करा @@ -127,8 +102,6 @@ आयात करा नोट लिस्ट मध्ये चिन्हांकित करा नोट्सच्या सूचीमध्ये चिन्हांकित स्वरूपन सक्षम करा - सर्वेक्षण भरा - आपल्याला काय आवडते ते आम्हाला सांगून अनुप्रयोग सुधारण्यात आम्हाला मदत करा टॅग्ज बदला @@ -137,8 +110,6 @@ मुक्त स्त्रोत प्रकल्पाबद्दल अधिक जाणून घ्या ग्रंथालये मुक्त स्रोत प्रकल्प - मुक्त स्त्रोत प्रकल्पाबद्दल - टीप स्मरणपत्र स्मरणपत्र तारीख स्मरणपत्र वेळ वारंवारिता पुनरावृत्ती करा @@ -153,8 +124,6 @@ इंटरफेस आणि अनुभव अॅप कसे दिसते आणि कसे वाटते ते निवडा - टीप प्राधान्ये - टीप आणि अन्य सेटिंग्ज निवडा विषयी आमच्याबद्दल आणि अॅपबद्दल अधिक जाणून घ्या डीफॉल्ट टीप रंग @@ -189,19 +158,6 @@ नोंदी जोडा चेकलिस्ट जोडा - ब्लॉक फॉरमॅटिंग - मथळा - उप शीर्षलेख - मजकूर - कोट - कोड - चेकलिस्ट - चिन्हांकित स्वरूपन - धीट - तिर्यक - अधोरेखित करा - स्ट्राइक - यादी एक टीप निवडा टिपा निवडा सेटिंग्ज @@ -215,29 +171,18 @@ काय नवीन आहे अॅपच्या अलीकडील अद्यतनांमध्ये नवीन काय आहे ते जाणून घ्या भाषांतर करा - एक नवीन टीप जोडण्यासाठी क्लिक करा - मुख्यपृष्ठ पर्याय पाहण्यासाठी क्लिक करा - होम मेनूमध्ये आपल्या पसंतीचे आणि संग्रहित नोट्स तसेच अनुप्रयोग सेटिंग्ज असतात. आपण येथे आपले टॅगदेखील शोधू शकता. अॅपमध्ये साइन इन करा मेघ बॅकअप आणि समक्रमण साठी साइन इन करा साइन आऊट करा मेघ बॅकअप थांबविण्यासाठी साइन आउट करा - Google सह साइन इन करा - साइन इन करत आहे … | - आपण लॉग इन केले आहे Google लॉगिन अयशस्वी गोपनीयता धोरण सामग्रीसाठी अॅप्स गोपनीयता धोरण प्रो अॅप स्थापित करा का प्रो स्थापित क्लाऊड सिंक चालविण्याकरिता मोठ्या सर्व्हरच्या खर्चासाठी समर्थन विकसक \n\n प्रथम नवीनतम वैशिष्ट्ये मिळवा \n\n काही अतिरिक्त वैशिष्ट्ये केवळ प्रो वापरकर्त्यांसाठी उपलब्ध असतील - मेघ समक्रमण सक्षम का - डिव्हाइस बदलांविषयी अपलोड आणि बॅकअप \n\n एकाधिक डिव्हाइसेस दरम्यान समक्रमण करा, आणि द्रुत अद्यतने मिळवा \n\n Google Firebase सर्व्हरवर सुरक्षितपणे जतन केले - हॅलो, - सामग्री टिपा आता कमी दर्जाची आहे! - सुरु करूया कॉपी करा @@ -254,30 +199,24 @@ मी समजून घ्या स्वयंचलितरित्या निर्यात करा बॅकअप म्हणून बाह्य फायलीमध्ये वारंवार नोट्स निर्यात करा - प्रतिमा प्रतिमा जोडण्यासाठी किंवा बदलण्यासाठी क्लिक करा फक्त एकदाच दैनिक - सानुकूल - बीटा स्मरणपत्रे आणि अलार्म %s मुक्त स्त्रोत आहे आणि कोणीही ते अधिक चांगले बनवण्यासाठी योगदान देण्यास तयार आहे. हे सध्या %s द्वारे बांधले आणि ठेवली जाते. %s ही एक सोपी टीप ठेवण्याचे अनुप्रयोग आहे. तो वापरण्यासाठी अतिशय कठीण अनुभव न जलद रिच मजकूर इनपुट परवानगी देते. हे बहु- tasking एक ब्रीझ करते हाय, आम्ही डिझायनर आणि प्रोग्रामरचा जोडीदार आहे, ज्याने %s तयार केले आहे. आम्ही सुरेख, काळजीपूर्वक डिझाइन केलेले विनामूल्य, जाहिरात-मुक्त किंवा कमीत कमी जाहिरातीचे अॅप्स तयार करण्याचा प्रयत्न करतो जे प्रत्येकाला उत्कृष्ट उपयुक्तता देतात! %d होम स्क्रीनवरील ओळी - %s शोधा … Distraction विनामूल्य - सेपरेटर अॅप थीम थीमसाठी पार्श्वभूमी रंग निवडा अॅप थीम निवडा टॅग जोडण्यासाठी किंवा बदलण्यासाठी क्लिक करा - अधिक क्रिया दर्शवा नोट्स निवडा एकाच वेळी एकाधिक नोट्सवर क्रिया निवडा आणि करा टीप कचर्यात हलविली गेली @@ -317,8 +256,6 @@ नोट्स विलीन करा मार्कडाउन म्हणून निर्यात करा मार्कडाउन स्वरूपनात नोट्स निर्यात करा. (आपण हे अॅपवर परत आयात करू शकत नाही) - फोल्डर निर्यात करा - सर्व निर्यात कोठे संग्रहित आहेत ते निवडा पार्श्वभूमीसाठी थीम रंग वापरा पार्श्वभूमीसाठी नोट रंग वापरा सक्षम करा diff --git a/base/src/main/res/values-ms/strings.xml b/base/src/main/res/values-ms/strings.xml index e1e9ec82..ddbcd833 100644 --- a/base/src/main/res/values-ms/strings.xml +++ b/base/src/main/res/values-ms/strings.xml @@ -6,17 +6,13 @@ Kongsi menggunakan … Tiada Nota Nampaknya anda belum menambahkan sebarang nota. Klik untuk menambah nota. - Tambah nota … - Tambah nota cepat … Edit Nota Buka Dalam Popup - Padam Nota Salin Nota Hantar Nota Pilih Tindakan … Tambah Kandungan … Tambah Tajuk … - Tambah Sub Tajuk … Tambahkan Teks Kutipan … Tambah Item … Tambah Kod … @@ -34,14 +30,10 @@ Masukkan PIN baru Masukkan PIN untuk membuka kunci Masukkan PIN semasa - masukkan kod Sahkan Buka kunci Sokongan Markdown Benarkan markup disokong pemformatan - Contoh - Tetapan Tamat Penamatan - Lihat cara menggunakan markdown dalam nota Tetapkan Dayakan Layout Senarai Tunjukkan nota dalam satu lajur @@ -67,23 +59,15 @@ Mengeksport ke failâ € | Selesai Kongsi - Mengenai Kami dan Lebih Banyak Rate pada Play Store Sumbang Versi App Nota Pencari Mengenai App - Mod Hari Tukar Warna - Ketik untuk melihat nota dalam mod hari Ketik untuk menukar warna latar belakang nota - Dayakan Mod Malam - Dayakan tema gelap sebagai lalai - Dayakan Mod Hari - Dayakan tema cahaya sebagai lalai - Mod malam Padamkan secara kekal Nota Unarkif Nota Arkib @@ -91,7 +75,6 @@ Tandakan Kegemaran Pulihkan Nota Pindah ke tong sampah - Ketik untuk melihat nota dalam mod malam Ketik untuk membuka nota untuk mengedit Ketik untuk menyalin kandungan nota ke papan keratan Ketik untuk berkongsi kandungan nota ke apl lain @@ -103,21 +86,13 @@ Ketik untuk menandakan nota sebagai kegemaran Ketik untuk mengarkibkan nota tersebut Ketik untuk menyusun notarkar nota - Nota andaâ € | Rumah - Semua nota biasa dan kegemaran anda Kegemaran - Semua nota kegemaran anda Diarkibkan - Semua nota arkib anda Sampah - Semua nota anda di dalam tong sampah Buat Pemberitahuan Pemberitahuan Nota Terkunci - Semua nota terkunci anda - Tags - Semua tanda nota anda Tambah Tag Baru Edit Tag … masukkan tag @@ -128,8 +103,6 @@ Import Senarai penurunan nilai dalam Senarai Nota Dayakan pemformatan keterlihatan dalam senarai nota - Isi kajian - Bantu kami meningkatkan aplikasi dengan memberitahu kami apa yang anda suka Tukar Tag @@ -138,8 +111,6 @@ Ketahui lebih lanjut mengenai projek sumber terbuka Perpustakaan Projek Sumber Terbuka - Mengenai Projek Sumber Terbuka - Peringatan Peringatan Tarikh Peringatan Masa Peringatan Kekerapan Ulang @@ -154,8 +125,6 @@ Antara Muka dan Pengalaman Pilih bagaimana aplikasinya kelihatan dan dirasakan. - Nota Pilihan - Pilih nota dan tetapan lain. Mengenai Ketahui lebih lanjut mengenai kami dan aplikasinya. Warna Nota Default @@ -190,19 +159,6 @@ Tambah Nota Tambah Senarai Semak - Blok Pemformatan - Tajuk - Sub Tajuk - Teks - Quote - Kod - Senarai Semak - Pemformatan Markdown - Bold - Italik - Garis bawah - Mogok - Senarai Pilih Nota Pilih Nota Tetapan @@ -216,29 +172,18 @@ Apa yang Baru Ketahui apa yang baru dalam kemas kini aplikasi baru-baru ini Terjemah - Klik untuk menambah nota baharu - Klik untuk melihat pilihan rumah - Menu rumah mempunyai nota kegemaran dan arkib anda, serta tetapan aplikasi. Anda boleh mencari tag anda di sini juga. Masuk ke App Masuk untuk sandaran dan penyegerakan awan Keluar Log keluar untuk menghentikan sandaran awan - Masuk dengan Google - Menandatangani dalam € | - Anda log masuk Masuk Google Gagal Dasar Privasi Dasar privasi apl untuk kandungan Pasang Pro App Mengapa Memasang Pro Pemaju sokongan untuk kos pelayan besar, untuk menjalankan penyegerakan awan \n\n Dapatkan ciri-ciri terkini terlebih dahulu \n\n Sesetengah ciri tambahan akan tersedia hanya untuk pengguna Pro - Kenapa membolehkan Cloud Sync - Muat naik dan sandaran terhadap perubahan peranti \n\n Segerakkan antara berbilang peranti, dan dapatkan kemas kini pantas \n\n Secepat disimpan di pelayan Google Firebase - Halo, - Nota Bahan kini Scarlet! - Bermula Salinan @@ -255,30 +200,24 @@ Saya faham Eksport Secara Automatik Nota eksport sering ke fail luaran sebagai sandaran - Gambar Klik untuk menambah atau menukar imej Hanya sekali Harian - Custom - Beta Peringatan dan Penggera %s adalah sumber terbuka dan sesiapa sahaja terbuka untuk menyumbang untuk menjadikannya lebih baik. Ia kini dibina dan diselenggarakan oleh %s. %s adalah aplikasi mengambil nota ringkas. Ia membolehkan input teks kaya cepat tanpa membuat pengalaman yang sangat sukar untuk digunakan. Ia membuat pelbagai tugas dengan mudah. Hai, kami sepasang pereka dan pengaturcara, yang mencipta %s. Kami berusaha untuk membina aplikasi iklan bebas, bebas iklan atau minimum yang direka dengan cermat, yang berfungsi dengan baik untuk semua orang! garis %d pada skrin utama - Cari %s … Penglibatan Percuma - Pemisah Tema App Pilih warna latar belakang untuk tema Pilih Tema App Klik untuk menambah atau tukar tag - Tunjukkan Lagi Tindakan Pilih Nota Pilih dan lakukan tindakan pada beberapa nota serentak Nota itu dipindahkan ke sampah @@ -318,8 +257,6 @@ Merge Notes Eksport Sebagai Penurunan Nilai Nota eksport dalam format markdown. (Anda tidak boleh mengimport kembali ini ke aplikasi) - Folder Eksport - Pilih di mana semua eksport disimpan Gunakan warna tema untuk latar belakang Gunakan warna nota untuk latar belakang Dayakan diff --git a/base/src/main/res/values-pa/strings.xml b/base/src/main/res/values-pa/strings.xml index 80b5a3ee..08d6b95d 100644 --- a/base/src/main/res/values-pa/strings.xml +++ b/base/src/main/res/values-pa/strings.xml @@ -6,17 +6,13 @@ ਵਰਤਦੇ ਹੋਏ ਸ਼ੇਅਰ ਕਰੋ … ਕੋਈ ਨੋਟਸ ਨਹੀਂ ਇੰਜ ਜਾਪਦਾ ਹੈ ਕਿ ਤੁਸੀਂ ਕੋਈ ਨੋਟ ਨਹੀਂ ਜੋੜਿਆ ਹੈ ਕੋਈ ਨੋਟ ਜੋੜਨ ਲਈ ਕਲਿੱਕ ਕਰੋ - ਕੋਈ ਨੋਟ ਜੋੜੋ … - ਇੱਕ ਤੁਰੰਤ ਸੂਚਨਾ ਜੋੜੋ … ਨੋਟ ਸੰਪਾਦਿਤ ਕਰੋ ਪੋਪਅੱਪ ਵਿੱਚ ਖੋਲ੍ਹੋ - ਨੋਟ ਮਿਟਾਓ ਨੋਟ ਕਾਪੀ ਕਰੋ ਸੂਚਨਾ ਭੇਜੋ ਕਾਰਵਾਈ ਚੁਣੋ … ਸਮੱਗਰੀ ਜੋੜੋ … ਸਿਰਲੇਖ ਸ਼ਾਮਲ ਕਰੋ … - ਸਬ ਸਿਰਲੇਖ ਸ਼ਾਮਲ ਕਰੋ … ਹਵਾਲੇ ਪਾਓ … ਆਈਟਮ ਸ਼ਾਮਲ ਕਰੋ … ਕੋਡ ਜੋੜੋ … @@ -35,7 +31,6 @@ ਨਵਾਂ PIN ਦਰਜ ਕਰੋ ਅਨਲੌਕ ਕਰਨ ਲਈ PIN ਦਰਜ ਕਰੋ ਮੌਜੂਦਾ PIN ਦਰਜ ਕਰੋ - ਕੋਡ ਦਰਜ ਕਰੋ ਜਾਂਚ ਕਰੋ ਅਨਲੌਕ ਸੈੱਟ ਕਰੋ @@ -46,9 +41,6 @@ ਥੋੜ੍ਹੇ ਸਮੇਂ ਵਿਚ ਗਰਿੱਡ ਵਿਚ ਨੋਟ ਦਿਖਾਓ ਮਾਰਕਡਾਉਨ ਸਹਿਯੋਗ ਮਾਰਕਡਾਊਨ ਸਮਰਥਿਤ ਫੌਰਮੈਟਿੰਗ ਨੂੰ ਅਨੁਮਤੀ ਦਿਓ - ਉਦਾਹਰਨਾਂ - ਮਾਰਕੇਡਾਊਨ ਸੈਟਿੰਗਜ਼ - ਸੂਚਨਾਵਾਂ ਵਿੱਚ ਮਾਰਕਡਾਊਨ ਨੂੰ ਕਿਵੇਂ ਵਰਤਿਆ ਜਾਵੇ ਵੇਖੋ ਚੋਣਾਂ ਅਤੇ ਸੈਟਿੰਗਾਂ ਨੋਟ ਐਕਸਪੋਰਟ ਸਾਂਝੇ ਕਰਨ ਲਈ ਡਿਵਾਈਸ ਸਟੋਰੇਜ ਨੂੰ ਨੋਟ ਐਕਸਪੋਰਟ ਕਰੋ @@ -68,22 +60,14 @@ ਫਾਈਲ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰ ਰਿਹਾ ਹੈ | ਹੋ ਗਿਆ ਸਾਂਝਾ ਕਰੋ - ਸਾਡੇ ਬਾਰੇ ਅਤੇ ਹੋਰ ਪਲੇ ਸਟੋਰ ਤੇ ਰੇਟ ਯੋਗਦਾਨ ਐਪ ਵਰਜ਼ਨ ਐਪ ਬਾਰੇ - ਦਿਵਸ ਮੋਡ ਰੰਗ ਬਦਲੋ - ਦਿਨ ਦੇ ਮੋਡ ਵਿੱਚ ਨੋਟ ਦੇਖਣ ਲਈ ਟੈਪ ਕਰੋ ਨੋਟ ਦੇ ਪਿਛੋਕੜ ਰੰਗ ਨੂੰ ਬਦਲਣ ਲਈ ਟੈਪ ਕਰੋ - ਰਾਤ ਮੋਡ ਸਮਰੱਥ ਬਣਾਓ - ਡਿਫੌਲਟ ਦੇ ਤੌਰ ਤੇ ਡੌਕ ਥੀਮ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ - ਦਿਨ ਮੋਡ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ - ਡਿਫਾਲਟ ਤੌਰ ਤੇ ਲਾਈਟ ਥੀਮ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ - ਨਾਈਟ ਮੋਡ ਹਮੇਸ਼ਾ ਲਈ ਹਟਾਓ ਅਨਾਰਚਾਇਜ਼ ਨੋਟ ਆਰਕਾਈਵ ਸੂਚਨਾ @@ -91,7 +75,6 @@ ਮਰਕੁਸ ਪਸੰਦੀਦਾ ਨੋਟ ਰੀਸਟੋਰ ਕਰੋ ਰੱਦੀ \'ਚ ਭੇਜੋ - ਰਾਤ ਨੂੰ ਮੋਡ ਵਿੱਚ ਨੋਟ ਦੇਖਣ ਲਈ ਟੈਪ ਕਰੋ ਸੰਪਾਦਨ ਲਈ ਨੋਟ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ ਸੂਚਨਾ ਸਮੱਗਰੀ ਦੀ ਕਲਿਪਬੋਰਡ ਤੇ ਕਾਪੀ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ ਹੋਰ ਐਪਸ ਤੇ ਨੋਟ ਸਮੱਗਰੀ ਨੂੰ ਸਾਂਝਾ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ @@ -103,21 +86,13 @@ ਨੋਟ ਨੂੰ ਮਨਪਸੰਦ ਨਾ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ ਨੋਟ ਨੂੰ ਅਕਾਇਵ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ ਨੋਟ ਨੂੰ ਅਨਾਰਿਚ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ - ਤੁਹਾਡੇ ਨੋਟਸ | ਘਰ - ਤੁਹਾਡੇ ਸਾਰੇ ਆਮ ਅਤੇ ਮਨਪਸੰਦ ਨੋਟਸ ਮਨਪਸੰਦ - ਤੁਹਾਡੇ ਸਾਰੇ ਪਸੰਦੀਦਾ ਨੋਟਸ ਆਰਕਾਈਵਡ - ਤੁਹਾਡੇ ਸਾਰੇ ਸੰਗ੍ਰਹਿਤ ਨੋਟਸ ਟ੍ਰੈਸ਼ - ਰੱਦੀ ਵਿੱਚ ਤੁਹਾਡੇ ਸਾਰੇ ਨੋਟਸ ਸੂਚਨਾ ਬਣਾਓ ਸੂਚਨਾ ਨੋਟਿਸ ਬੰਦ - ਤੁਹਾਡੇ ਸਾਰੇ ਲਾਕ ਨੋਟਸ - ਟੈਗਸ - ਤੁਹਾਡੇ ਸਾਰੇ ਨੋਟ ਟੈਗਸ ਨਿਊ ਟੈਗ ਸ਼ਾਮਲ ਕਰੋ | ਟੈਗ ਸੰਪਾਦਨ ਕਰੋ | ਟੈਗ ਦਿਓ @@ -128,8 +103,6 @@ ਆਯਾਤ ਕਰੋ ਨੋਟ ਸੂਚੀ ਵਿੱਚ ਮਾਰਕੇਡਾਊਨ ਨੋਟਸ ਦੀ ਸੂਚੀ ਵਿੱਚ ਮਾਰਕੱਡਾਡ ਫਾਰਮੈਟਿੰਗ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ - ਸਰਵੇਖਣ ਭਰੋ - ਸਾਨੂੰ ਦੱਸੋ ਕਿ ਤੁਸੀਂ ਕੀ ਪਸੰਦ ਕਰਦੇ ਹੋ, ਐਪ ਨੂੰ ਬਿਹਤਰ ਬਣਾਉਣ ਵਿੱਚ ਸਾਡੀ ਸਹਾਇਤਾ ਕਰੋ ਟੈਗ ਬਦਲੋ @@ -138,8 +111,6 @@ ਓਪਨ ਸੋਰਸ ਪ੍ਰੋਜੈਕਟ ਬਾਰੇ ਹੋਰ ਜਾਣੋ ਲਾਇਬ੍ਰੇਰੀਆਂ ਓਪਨ ਸੋਰਸ ਪ੍ਰੋਜੈਕਟ - ਓਪਨ ਸੋਰਸ ਪ੍ਰੋਜੈਕਟ ਬਾਰੇ - ਨੋਟ ਰੀਮਾਈਂਡਰ ਰੀਮਾਈਂਡਰ ਤਾਰੀਖ ਰੀਮਾਈਂਡਰ ਟਾਈਮ ਬਾਰ ਬਾਰ ਦੁਹਰਾਓ @@ -154,8 +125,6 @@ ਇੰਟਰਫੇਸ ਅਤੇ ਅਨੁਭਵ ਚੁਣੋ ਕਿ ਐਪ ਕਿਵੇਂ ਵੇਖਦਾ ਹੈ ਅਤੇ ਕਿਵੇਂ ਮਹਿਸੂਸ ਕਰਦਾ ਹੈ - ਨੋਟ ਪਸੰਦ - ਨੋਟ ਅਤੇ ਹੋਰ ਸੈਟਿੰਗਜ਼ ਚੁਣੋ. ਇਸ ਬਾਰੇ ਸਾਡੇ ਅਤੇ ਐਪ ਬਾਰੇ ਹੋਰ ਜਾਣੋ ਡਿਫੌਲਟ ਨੋਟ ਰੰਗ @@ -190,19 +159,6 @@ ਨੋਟ ਜੋੜੋ ਚੈੱਕਲਿਸਟ ਸ਼ਾਮਲ ਕਰੋ - ਬਲਾਕ ਫਾਰਮੈਟਿੰਗ - ਸਿਰਲੇਖ - ਉਪ ਸਿਰਲੇਖ - ਟੈਕਸਟ - ਹਵਾਲਾ - ਕੋਡ - ਚੈੱਕਲਿਸਟ - ਮਾਰਕੇਡੋਂ ਫਾਰਮੈਟਿੰਗ - ਬੋਲਡ - ਇਟਾਲਿਕ - ਹੇਠਾਂ ਰੇਖਾ ਖਿੱਚੋ - ਹੜਤਾਲ - ਸੂਚੀ ਇੱਕ ਨੋਟ ਚੁਣੋ ਨੋਟਸ ਚੁਣੋ ਸੈਟਿੰਗਾਂ @@ -216,9 +172,6 @@ ਨਵਾਂ ਕੀ ਹੈ ਐਪ ਦੇ ਹਾਲ ਹੀ ਦੇ ਅਪਡੇਟਸ ਵਿੱਚ ਨਵਾਂ ਕੀ ਹੈ ਪਤਾ ਕਰੋ ਅਨੁਵਾਦ - ਇੱਕ ਨਵਾਂ ਨੋਟ ਜੋੜਨ ਲਈ ਕਲਿੱਕ ਕਰੋ - ਘਰ ਦੀਆਂ ਚੋਣਾਂ ਦੇਖਣ ਲਈ ਕਲਿੱਕ ਕਰੋ - ਹੋਮ ਮੀਨੂ ਵਿੱਚ ਤੁਹਾਡੀ ਮਨਪਸੰਦ ਅਤੇ ਅਕਾਇਵ ਨੋਟਸ ਦੇ ਨਾਲ ਨਾਲ ਐਪਲੀਕੇਸ਼ਨ ਸੈਟਿੰਗਜ਼ ਵੀ ਹਨ. ਤੁਸੀਂ ਇੱਥੇ ਆਪਣੇ ਟੈਗ ਵੀ ਲੱਭ ਸਕਦੇ ਹੋ @@ -236,51 +189,37 @@ ਮੈਂ ਸੱਮਝਦਾ ਹਾਂ ਆਟੋਮੈਟਿਕ ਤੌਰ ਤੇ ਨਿਰਯਾਤ ਕਰੋ ਬੈਕਅਪ ਦੇ ਤੌਰ ਤੇ ਬਾਹਰੀ ਫਾਈਲਾਂ ਤੇ ਅਕਸਰ ਨੋਟ ਐਕਸਪੋਰਟ ਕਰੋ - ਚਿੱਤਰ ਚਿੱਤਰ ਨੂੰ ਜੋੜਨ ਜਾਂ ਬਦਲਣ ਲਈ ਕਲਿੱਕ ਕਰੋ ਸਿਰਫ਼ ਇੱਕ ਵਾਰ ਰੋਜ਼ਾਨਾ - ਕਸਟਮ - ਬੀਟਾ ਰੀਮਾਈਂਡਰਸ ਅਤੇ ਅਲਾਰਮ %s ਓਪਨ ਸੋਰਸ ਹੈ ਅਤੇ ਕੋਈ ਵੀ ਇਸ ਨੂੰ ਬਿਹਤਰ ਬਣਾਉਣ ਲਈ ਯੋਗਦਾਨ ਪਾਉਣ ਲਈ ਖੁੱਲ੍ਹਾ ਹੈ. ਇਹ ਵਰਤਮਾਨ ਵਿੱਚ %s ਦੁਆਰਾ ਬਣਾਇਆ ਅਤੇ ਪਰਬੰਧਨ ਕੀਤਾ ਗਿਆ ਹੈ. %s ਇੱਕ ਸਧਾਰਨ ਨੋਟ ਲੈਣਾ ਐਪ ਹੈ. ਇਹ ਤਜ਼ਰਬੇ ਨੂੰ ਬਹੁਤ ਮੁਸ਼ਕਲ ਬਣਾਉਣ ਦੇ ਬਿਨਾਂ ਤੁਰੰਤ ਰਿਚ ਟੈਕਸਟ ਇੰਪੁੱਟ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ. ਇਹ ਬਹੁਤਾ ਨਾਲ ਕੰਮ ਕਰਨ ਵਾਲੀ ਇੱਕ ਹਵਾ ਬਣਾਉਂਦਾ ਹੈ ਹੈਲੋ, ਅਸੀਂ ਡਿਜ਼ਾਇਨਰ ਅਤੇ ਪ੍ਰੋਗਰਾਮਰ ਦਾ ਇੱਕ ਜੋੜਾ ਹਾਂ, ਜਿਸਨੇ %s ਬਣਾਇਆ ਹੈ ਅਸੀਂ ਸੁੰਦਰ, ਧਿਆਨ ਨਾਲ ਡਿਜ਼ਾਈਨ ਕੀਤੇ ਗਏ ਮੁਫ਼ਤ, ਵਿਗਿਆਪਨ-ਮੁਕਤ ਜਾਂ ਘੱਟੋ ਘੱਟ ਇਸ਼ਤਿਹਾਰਾਂ ਵਾਲੇ ਐਪਸ ਬਣਾਉਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਦੇ ਹਾਂ ਜੋ ਹਰ ਕਿਸੇ ਲਈ ਮਹਾਨ ਉਪਯੋਗਤਾ ਦੀ ਸੇਵਾ ਕਰਦੇ ਹਨ! %d ਹੋਮ ਸਕ੍ਰੀਨ ਤੇ ਲਾਈਨਾਂ - %s ਖੋਜ ਕਰੋ … ਵਿਵਹਾਰ ਮੁਫ਼ਤ - ਵਿਭਾਜਕ ਐਪ ਵਿੱਚ ਸਾਈਨ ਇਨ ਕਰੋ ਕਲਾਉਡ ਬੈਕਅਪ ਅਤੇ ਸਮਕਾਲੀਕਰਨ ਲਈ ਸਾਈਨ ਇਨ ਕਰੋ ਸਾਇਨ ਆਉਟ ਕਲਾਉਡ ਬੈਕਅਪ ਨੂੰ ਰੋਕਣ ਲਈ ਸਾਈਨ ਆਉਟ ਕਰੋ - Google ਦੇ ਨਾਲ ਸਾਈਨ ਇਨ ਕਰੋ - ਸਾਈਨ ਇੰਨ ਕਰੋ | - ਤੁਸੀਂ ਲੌਗਇਨ ਹੋ ਗੂਗਲ ਲਾਗਇਨ ਫੇਲ੍ਹ ਪਰਾਈਵੇਟ ਨੀਤੀ ਸਮਗਰੀ ਲਈ ਐਪ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਪ੍ਰੋ ਐੱਸ ਇੰਸਟਾਲ ਕਰੋ ਪ੍ਰੋ ਇੰਸਟਾਲ ਕਿਉਂ ਕਰੋ ਕਲਾਉਡ ਸੰਕੁਚਨ ਨੂੰ ਚਲਾਉਣ ਲਈ ਵੱਡੇ ਸਰਵਰ ਲਾਗਤਾਂ ਲਈ ਸਹਿਯੋਗ ਡਿਵੈਲਪਰ \n\n ਪਹਿਲਾਂ ਸਭ ਤੋਂ ਨਵੀਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਪ੍ਰਾਪਤ ਕਰੋ \n\n ਕੁਝ ਹੋਰ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਕੇਵਲ ਪ੍ਰੋ ਯੂਜ਼ਰਸ ਲਈ ਉਪਲਬਧ ਹੋਣਗੀਆਂ - ਕਿਉਂ Cloud Sync ਸਮਰਥਿਤ ਕਰੋ - ਡਿਵਾਈਸ ਬਦਲਾਅ ਦੇ ਵਿਰੁੱਧ ਅਪਲੋਡ ਅਤੇ ਬੈਕਅੱਪ ਕਰੋ \n\n ਬਹੁਤੇ ਡਿਵਾਈਸਾਂ ਵਿਚਕਾਰ ਸਿੰਕ ਕਰੋ, ਅਤੇ ਤੁਰੰਤ ਅਪਡੇਟਾਂ ਪ੍ਰਾਪਤ ਕਰੋ \n\n Google Firebase ਸਰਵਰਾਂ ਤੇ ਸੁਰੱਖਿਅਤ ਰੂਪ ਵਿੱਚ ਸੁਰੱਖਿਅਤ ਕੀਤਾ ਗਿਆ - ਸਤ ਸ੍ਰੀ ਅਕਾਲ, - ਮੈਟੀਰੀਅਲ ਨੋਟਸ ਹੁਣ ਸਵਾਨੇ ਹਨ! - ਸ਼ੁਰੂਆਤ ਕਰੋ ਐਪ ਥੀਮ ਥੀਮ ਲਈ ਬੈਕਗਰਾਊਂਡ ਰੰਗ ਚੁਣੋ ਐਪ ਥੀਮ ਚੁਣੋ ਟੈਗਸ ਨੂੰ ਜੋੜਨ ਜਾਂ ਬਦਲਣ ਲਈ ਕਲਿਕ ਕਰੋ - ਹੋਰ ਕਾਰਵਾਈਆਂ ਦਿਖਾਓ ਨੋਟਸ ਚੁਣੋ ਇੱਕ ਵਾਰ ਵਿੱਚ ਕਈ ਨੋਟਿਸਾਂ ਉੱਤੇ ਕਾਰਵਾਈ ਚੁਣੋ ਅਤੇ ਕਾਰਵਾਈ ਕਰੋ ਨੋਟ ਨੂੰ ਰੱਦੀ \'ਚ ਭੇਜਿਆ ਗਿਆ ਸੀ @@ -320,8 +259,6 @@ ਨੋਟਸ ਮਿਲਾਓ ਮਾਰਕਡਾਉਨ ਦੇ ਤੌਰ ਤੇ ਨਿਰਯਾਤ ਕਰੋ ਮਾਰਕੇਡਾਊਨ ਫਾਰਮੈਟ ਵਿੱਚ ਨੋਟ ਨਿਰਯਾਤ ਕਰੋ. (ਤੁਸੀਂ ਇਹ ਵਾਪਸ ਐਪ ਤੇ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕਦੇ) - ਫੋਲਡਰ ਨਿਰਯਾਤ ਕਰੋ - ਚੁਣੋ ਕਿ ਸਾਰੇ ਨਿਰਯਾਤ ਕਿੱਥੇ ਸਟੋਰ ਕੀਤੇ ਜਾਂਦੇ ਹਨ ਪਿਛੋਕੜ ਲਈ ਥੀਮ ਕਲੰਡ ਵਰਤੋਂ ਪਿਛੋਕੜ ਲਈ ਨੋਟ ਰੰਗ ਦੀ ਵਰਤੋਂ ਕਰੋ ਸਮਰੱਥ ਬਣਾਓ diff --git a/base/src/main/res/values-pl/strings.xml b/base/src/main/res/values-pl/strings.xml index a8489a60..36958ba7 100644 --- a/base/src/main/res/values-pl/strings.xml +++ b/base/src/main/res/values-pl/strings.xml @@ -6,20 +6,14 @@ Zapisać Import Szukaj w notatkach… - Szukaj %s… Udostępnij za pomocą… Brak notatek Wygląda na to, że nie dodałeś żadnych notatek. Kliknij, aby dodać notatkę. - Dodaj notatkę… - Dodaj szybką notatkę… Dodaj notatkę Dodaj listę kontrolną - Tryb nocny - Tryb dzienny Edytuj notatkę Utwórz powiadomienie Otwórz w Popup - Usuń notatkę Usuń trwale Kopiuj notatkę Wyślij notatkę @@ -41,8 +35,6 @@ Distraction Free Scal notatki Wybierz działanie… - Dotknij, aby wyświetlić notatkę w trybie nocnym - Dotknij, aby wyświetlić notatkę w trybie dziennym Dotknij, aby otworzyć notatkę do edycji Dotknij, aby skopiować zawartość notatki do schowka Dotknij, aby udostępnić treść notatki innym aplikacjom @@ -55,58 +47,26 @@ Wybierz, aby zarchiwizować notatkę Dotknij, aby zmienić kolor tła notatki Dotknij, aby rozpakować notatkę - Twoje notatki i więcej… Dom - Wszystkie twoje normalne i ulubione notatki Ulubione - Wszystkie twoje ulubione notatki Zarchiwizowane - Wszystkie zarchiwizowane notatki Zablokowany - Wszystkie twoje zablokowane notatki - Tagi - Wszystkie twoje tagi notatek Śmieci - Wszystkie twoje notatki w koszu Dodaj treść… Dodaj nagłówek… - Dodaj podpozycję… Dodaj cytowany tekst… Dodaj element… Dodaj kod… Kliknij, aby dodać lub zmienić obraz - Formatowanie bloku - Nagłówek - Podtytuł - Tekst - Zacytować - Kod - Lista kontrolna - Obraz - Separator - Formatowanie w dół - Pogrubienie - Kursywa - Podkreślać - Strajk - Lista Opcje i ustawienia Interfejs i doświadczenie Wybierz sposób, w jaki aplikacja wygląda i czuje się. - Preferencje notatki - Wybierz notatkę i inne ustawienia. O Dowiedz się więcej o nas i aplikacji. Bezpieczeństwo Blokowanie notatek i opcji zabezpieczeń - Włącz tryb nocny - Włącz ciemny motyw jako domyślny - Włącz tryb dzienny - Włącz domyślny motyw światła - Ustawienia przeceny - Zobacz, jak używać notowań w notowaniach Włącz układ listy Pokaż notatki w jednej kolumnie Włącz układ siatki @@ -120,8 +80,6 @@ Importuj notatki z pamięci urządzenia Eksportuj jako Markdown Eksportuj notatki w formacie przeceny. (Nie możesz zaimportować ich z powrotem do aplikacji) - Eksportuj folder - Wybierz miejsce przechowywania wszystkich eksportów Eksportuj automatycznie Często eksportuj notatki do pliku zewnętrznego jako kopię zapasową O nas @@ -130,8 +88,6 @@ Dowiedz się więcej o projekcie open source Oceń i zrecenzuj Powiedz nam, ile podobała ci się aplikacja - Wypełnij ankietę - Pomóż nam ulepszyć aplikację, mówiąc nam, co lubisz Zainstaluj Pro App Zainstaluj Pro App, aby odblokować funkcje i wsparcie programisty Migracja notatek do Scarlet Pro @@ -161,7 +117,6 @@ Wprowadź nowy kod PIN Wprowadź kod PIN, aby odblokować Wprowadź bieżący kod PIN - Wprowadź kod Zweryfikować Odblokować Zestaw @@ -174,7 +129,6 @@ Zezwalaj na formatowanie obsługiwane przez przecenę Markdown in Note List Włącz formatowanie Markdown na liście notatek - Przykłady Daj uprawnienia Importowanie i eksportowanie wymaga uprawnień do przechowywania. Proszę o wyrażenie zgody na żądanie. Dopuszczać @@ -194,22 +148,17 @@ Zsynchronizuj wszystkie notatki, tagi itp. Z folderem zewnętrznym Eksportuj zablokowane notatki Wykonaj również kopię zapasową zablokowanych notatek - O nas i więcej Oceń na Play Store Przyczynić się Wersja aplikacji O aplikacji Biblioteki Projekt Open Source - O projekcie Open Source - Uwaga Przypomnienie Data przypomnienia Czas przypomnienia Powtórz częstotliwość Tylko raz Codziennie - Zwyczaj - Beta Uwaga Powiadomienia Przypomnienia i alarmy %s jest open source i każdy jest otwarty, aby przyczynić się do jego ulepszenia. Jest obecnie budowany i utrzymywany przez %s. @@ -235,9 +184,6 @@ Co nowego Dowiedz się, co nowego w najnowszych aktualizacjach aplikacji Tłumaczyć - Kliknij, aby dodać nową notatkę - Kliknij, aby zobaczyć opcje domu - Menu główne zawiera ulubione i archiwalne notatki, a także ustawienia aplikacji. Tutaj możesz znaleźć swoje tagi. Kopiuj Blokuj akcje tekstowe Galeria @@ -273,9 +219,6 @@ Zaloguj się, aby wykonać kopię zapasową w chmurze i synchronizację Wyloguj się Wyloguj się, aby zatrzymać tworzenie kopii zapasowej w chmurze - Zaloguj się za pomocą Google - Podpisywanie… - Jesteś zalogowany Logowanie Google nie powiodło się Polityka prywatności Polityka prywatności aplikacji dla treści @@ -283,18 +226,12 @@ Dostępne na Scarlet Pro Dlaczego warto zainstalować Pro? ✔ Wspieraj programistę w zakresie ogromnych kosztów serwera, aby uruchomić synchronizację w chmurze \n\n✔ Najpierw zdobądź najnowsze funkcje\n\n✔ Niektóre dodatkowe funkcje będą dostępne tylko dla użytkowników Pro - Dlaczego warto włączyć Cloud Sync - ✔ Przesyłanie i tworzenie kopii zapasowych przed zmianami urządzeń \n\n✔ Synchronizacja pomiędzy wieloma urządzeniami i szybkie aktualizacje \n\n✔ Bezpieczne zapisywanie na serwerach Google Firebase - Dzień dobry, - Notatki materiałowe są teraz Scarlet! - Zaczynać Motyw aplikacji Wybierz kolor tła dla motywu Wybierz motyw aplikacji Kliknij, aby dodać lub zmienić tagi - Pokaż więcej działań Wybierz Notatki Wybierz i wykonaj czynności na wielu notatkach jednocześnie Notatka została przeniesiona do kosza diff --git a/base/src/main/res/values-pt/strings.xml b/base/src/main/res/values-pt/strings.xml index bf69df6a..e4904f47 100644 --- a/base/src/main/res/values-pt/strings.xml +++ b/base/src/main/res/values-pt/strings.xml @@ -6,25 +6,18 @@ Compartilhe usando … Sem notas Parece que você não adicionou nenhuma nota. Clique para adicionar uma nota. - Adicione uma anotação… - Adicione uma nota rápida … Editar nota Abrir em pop-up - Excluir nota Copiar nota Enviar nota Escolher ação… Adicionar conteúdo… Adicionar cabeçalho … - Adicionar sub encabeçamento … Adicionar texto citado … Adicionar Item… Adicionar código … Suporte Markdown Permitir formatação suportada pelo markdown - Exemplos - Configurações de Markdown - Veja como usar markdown em notas Opções e Configurações Exportar notas Exportar notas para o armazenamento do dispositivo para compartilhar @@ -44,7 +37,6 @@ Exportando para arquivo … Feito Compartilhar - Sobre nós e mais Avalie na Play Store Contribuir Versão da aplicação @@ -69,22 +61,14 @@ Digite o novo PIN Digite PIN para desbloquear Digite o PIN atual - Coloque o código Verificar Desbloquear Conjunto Remover - Modo dia Mudar cor - Toque para visualizar a nota no modo dia Toque para alterar a cor de fundo da nota - Ativar modo noturno - Ativar tema escuro como padrão - Ativar modo dia - Ative o tema da luz como padrão - Modo noturno Apagar permanentemente Unarchive Note Nota de arquivo @@ -92,7 +76,6 @@ Marcar Favorito Nota de restauração Mover para lixeira - Toque para ver a nota no modo noturno Toque para abrir a nota para edição Toque para copiar o conteúdo da nota para a área de transferência Toque para compartilhar conteúdo de notas em outras aplicações @@ -104,21 +87,13 @@ Toque para marcar a nota não como favorito Toque para arquivar a nota Toque para desarchivar a nota - Suas notas … Casa - Todas as suas notas normais e favoritas Favoritos - Todas as suas notas favoritas Arquivado - Todas as suas anotações arquivadas Lixo - Todas as suas notas no lixo Criar Notificação Notificações de nota Bloqueado - Todas as suas notas bloqueadas - Tag - Todas as suas notas Adicionar nova etiqueta … Editar Tag … digite tag @@ -129,8 +104,6 @@ Importar Marcação na lista de notas Habilite a formatação do Markdown na lista de notas - Enquete de preenchimento - Ajude-nos a melhorar o aplicativo, dizendo-nos o que você gosta Alterar Tags @@ -139,8 +112,6 @@ Saiba mais sobre o projeto de código aberto Bibliotecas Projeto de código aberto - Sobre o Projeto Open Source - Lembrete de nota Data de Lembrete Tempo de lembrete Frequência repetida @@ -155,8 +126,6 @@ Interface e Experiência Escolha como o aplicativo parece e se sente. - Preferências de nota - Escolha a nota e outras configurações. Sobre Saiba mais sobre nós e o aplicativo. Cor da nota padrão @@ -191,19 +160,6 @@ Adicionar nota Adicionar lista de verificação - Formatação de blocos - Título - Título secundário - Texto - Citar - Código - Lista de controle - Formatação de Markdown - Negrito - Itálico - Sublinhado - Greve - Lista Selecione uma nota Selecione notas Configurações @@ -217,9 +173,6 @@ O que há de novo Saiba o que há de novo nas atualizações recentes do aplicativo Traduzir - Clique para adicionar uma nova nota - Clique para ver as opções de casa - O menu inicial tem suas notas favoritas e de arquivo, bem como as configurações da aplicação. Você também pode encontrar suas tags aqui. @@ -237,51 +190,37 @@ Compreendo Exportar automaticamente Exportar notas com freqüência para arquivos externos como backup - Imagem Clique para adicionar ou alterar a imagem Apenas uma vez Diariamente - personalizadas - Beta Lembretes e alarmes %s é de código aberto e qualquer pessoa está aberta para contribuir para torná-lo melhor. Atualmente, ele é construído e mantido por %s. %s é um simples aplicativo de tomada de notas. Ele permite uma entrada rápida de Rich Text sem tornar a experiência muito difícil de usar. Isso torna a multitarefa uma brisa. Oi, somos um par de designers e programadores, que criaram %s. Nós nos esforçamos para criar aplicativos de anúncios bonitos, cuidadosamente projetados, gratuitos, gratuitos ou mínimos, que servem uma ótima utilidade para todos! %d linhas na tela inicial - Pesquisar %s … Distração grátis - Separador Iniciar sessão na aplicação Faça login para o backup e sincronização da nuvem Sair Sair para parar o backup da nuvem - Faça login no Google - Assinando em … - Você está logado Falha no Login do Google Política de Privacidade Política de privacidade da aplicação para o conteúdo Instale a aplicação Pro Por que instalar o Pro Desenvolvedor de suporte para os custos maciços do servidor, para executar a sincronização da nuvem \n\n Obtenha os recursos mais recentes primeiro \n\n Alguns recursos extras estarão disponíveis apenas para usuários do Pro - Por que ativar o Cloud Sync - Carregar e fazer backup de mudanças no dispositivo \n\n Sincronizar entre vários dispositivos e obter atualizações rápidas \n\n Salvaguardado de forma segura em servidores do Google Firebase - Olá, - Notas de material agora é escarlate! - Iniciar App Theme Selecione a cor de fundo para o tema Selecione o tema da aplicação Clique para adicionar ou alterar tags - Mostrar mais ações Selecione Notas Selecione e execute ações em várias notas de uma só vez A nota foi movida para o lixo @@ -321,8 +260,6 @@ Mesclar anotações Exportar como Markdown Exportar notas no formato de marcação. (Você não pode importar de volta para o aplicativo) - Pasta de Exportação - Escolha onde todas as exportações são armazenadas Use a cor do tema para o fundo Use a cor da nota para o fundo Habilitar diff --git a/base/src/main/res/values-ru/strings.xml b/base/src/main/res/values-ru/strings.xml index 7695f943..dd338963 100644 --- a/base/src/main/res/values-ru/strings.xml +++ b/base/src/main/res/values-ru/strings.xml @@ -4,24 +4,17 @@ Сохранить Импортировать Поиск заметок - Поиск %s … Отправить через … Нет заметок Похоже, у вас нет ни одной заметки. Нажмите, чтобы добавить. - Добавить заметку… - Добавить быструю заметку… - Добавить заметку Добавить контрольный список - Ночной режим - Дневной режим Редактировать Создать уведомление Открыть всплывающее окно - Удалить Удалить совсем Копировать Поделиться @@ -43,8 +36,6 @@ Отвлечение бесплатно Выберите действие… - Нажмите, чтобы просмотреть заметку в ночном режиме - Нажмите, чтобы просмотреть заметку в дневном режиме Нажмите, чтобы открыть заметку для редактирования Нажмите, чтобы скопировать содержимое заметки в буфер обмена Нажмите, чтобы поделиться содержимым заметки с другим приложением @@ -58,60 +49,26 @@ Нажмите, чтобы сменить цвет фона заметки Нажмите, чтобы разархивировать заметку - Ваши заметки… Главное - Все ваши обычные и избранные заметки Избранное - Все ваши избранные заметки Архив - Все ваши архивные заметки Заблокированные - Все ваши заблокированные заметки - Теги - Все ваши заметки Корзина - Все ваши удалённые заметки Добавить содержимое… Добавить заголовок… - Добавить подзаголовок… Добавить цитируемый текст… Добавить пункт… Добавить код… Нажмите, чтобы добавить или изменить изображение - Блокирование форматирования - заголовок - Sub Heading - Текст - котировка - Код - контрольный список - Образ - Разделитель - - Форматирование Markdown - Жирный - Курсив - подчеркивание - удар - Список - Параметры и настройки Интерфейс и опыт Выберите, как приложение выглядит и чувствует. - Примечание. - Выберите примечание и другие настройки. О программе Узнайте больше о нас и приложении. Безопасность Блокировка заметок и параметров безопасности - Ночной режим - Включить темную тему по умолчанию - Дневной режим - Включить светлую тему по умолчанию - Настройки разметки - Посмотрите, как использовать уценку в заметках Включить макет списка Показывать заметки в одном столбце Включить компоновку сетки @@ -131,8 +88,6 @@ Узнайте больше о проекте с открытым исходным кодом Оценка и отзыв Расскажите нам насколько вам нравится приложение - Заполнение опроса - Помогите нам улучшить приложение, сообщив нам, что вам нравится Установить Pro App Установите приложение Pro для разблокировки функций и поддержки разработчика Перенос заметок в Scarlet Pro @@ -161,7 +116,6 @@ Введите новый PIN-код Введите PIN-код для разблокировки Введите текущий PIN-код - введите код проверить отпереть Задавать @@ -176,7 +130,6 @@ Разрешить форматирование поддерживаемых меток Отметка в списке заметок Включить форматирование Markdown в списке заметок - Примеры Дать разрешение Импорт и экспорт требуют разрешения на доступ к файловой системе. Пожалуйста, дайте разрешение по запросу. @@ -191,7 +144,6 @@ Поделиться - О нас и многое другое Оценить в Play Маркет Сделать вклад Версия приложения @@ -199,16 +151,12 @@ Библиотеки Проект с открытым исходным кодом - О проекте с открытым исходным кодом - Примечание Напоминание Дата напоминания Время напоминания Частота повторения Только один раз Ежедневно - изготовленный на заказ - Бета Уведомления об уведомлениях Напоминания и сигналы тревоги @@ -241,10 +189,6 @@ Узнайте, что нового в последних обновлениях приложения Переведите - Нажмите, чтобы добавить новую заметку - Нажмите, чтобы увидеть домашние параметры - В главном меню есть ваши любимые и архивные заметки, а также настройки приложения. Здесь вы можете найти свои теги. - копия Блокировать текстовые действия Галерея @@ -299,9 +243,6 @@ Вход для резервного копирования и синхронизации облаков Выход Выйти, чтобы остановить резервное копирование облаков - Войти с помощью Google - Вход в систему | - Вы вошли в систему Ошибка входа в Google политика конфиденциальности @@ -315,19 +256,10 @@ Сначала получить самые последние функции \n \n Некоторые дополнительные функции будут доступны только пользователям Pro - Зачем использовать Cloud Sync - Загрузка и резервное копирование от изменений устройства \n - \n - Синхронизация между несколькими устройствами и быстрое обновление \n - \n - Безопасное сохранение на серверах Google Firebase - Здравствуйте, - Материал Notes теперь Scarlet! - Начать @@ -338,7 +270,6 @@ Нажмите, чтобы добавить или изменить теги - Показать другие действия Выберите заметки Выбирайте и выполняйте действия сразу по нескольким нотам @@ -365,8 +296,6 @@ Объединить заметки Экспортировать как уценку Экспорт заметок в формате уценки. (Вы не можете импортировать их обратно в приложение) - Папка экспорта - Выберите, где хранится весь экспорт Используйте цвет темы для фона Используйте цвет заметки для фона включить diff --git a/base/src/main/res/values-tr/strings.xml b/base/src/main/res/values-tr/strings.xml index d14a25cf..7ff62532 100644 --- a/base/src/main/res/values-tr/strings.xml +++ b/base/src/main/res/values-tr/strings.xml @@ -6,17 +6,13 @@ Kullanarak paylaş … Not Yok Hiçbir not eklememişsiniz gibi görünüyor. Not eklemek için tıklayın. - Bir not ekle… - Hızlı not ekle … Notu Düzenle Açılır Pencerede Aç - Not Sil Notu Kopyala Not gönder Eylem seçin… İçerik Ekle … Başlık Ekle … - Alt Başlık Ekle … Alıntı Metni Ekle … Öğe eklemek… Kod Ekle … @@ -34,7 +30,6 @@ Yeni PIN girin Kilidi açmak için PIN girin Geçerli PIN\'i gir - Kodu girin DOĞRULAYIN Kilidini aç Set @@ -63,23 +58,15 @@ Dosyaya dışa aktarma | tamam Pay - Hakkımızda ve Daha Fazlası Play Store\'da Ücretlendirme Katkıda bulunmak Uygulama sürümü App Hakkında - Gün Modu Notları Ara … Rengi değiştir - Notu gündüz modunda görüntülemek için dokunun Notun arka plan rengini değiştirmek için dokunun - Gece Modunu Etkinleştir - Koyu temayı varsayılan olarak etkinleştir - Gün Modunu Etkinleştir - Hafif temayı varsayılan olarak etkinleştir - Gece modu kalıcı olarak sil Unarchive Not Arşiv Notu @@ -87,7 +74,6 @@ Favoriyi işaretle Notu Geri Yükle Çöp kutusuna taşıyın - Notu gece modunda görüntülemek için dokunun Düzenlemek için notu açmak için dokunun Not içeriğini panoya kopyalamak için dokunun Not içeriğini diğer uygulamalara paylaşmak için dokunun @@ -99,26 +85,15 @@ Notu favori olarak işaretlemek için dokunun Notu arşivlemek için hafifçe dokunun Notu arşivden kaldırmak için hafifçe dokunun - Notlarınız | Ev - Normal ve favori notlarınızın tümü Favoriler - En sevdiğiniz notların tümü Arşivlenen - Arşivlenen notlarınızın tamamı Çöp - Çöplerdeki notların tümü Markdown Destek Markdown destekli formatlamaya izin ver - Örnekler - İşaretleme Ayarları - Notlarda markdown\'u nasıl kullanacağınızı öğrenin Bildirim Oluştur Not Bildirimleri Kilitli - Kilitli notların tümü - Etiketler - Tüm not etiketleriniz Yeni Etiket Ekle | Etiketi Düzenle | etiketi gir @@ -129,8 +104,6 @@ İthalat Not Listesindeki İşaretleme Not listesinde Markdown biçimlendirmeyi etkinleştirin - Dolgu Anketi - Neye benzediğinizi söyleyerek uygulamayı geliştirmemize yardımcı olun Etiketleri Değiştir @@ -139,8 +112,6 @@ Açık kaynak projesi hakkında daha fazla şey öğrenin Kütüphaneler Açık Kaynak Projesi - Açık Kaynak Projesi Hakkında - Not Hatırlatma Hatırlatma Tarihi Hatırlatıcı Saati Sıklığı Tekrarla @@ -155,8 +126,6 @@ Arayüz ve Deneyim Uygulamanın nasıl göründüğünü ve neler hissettiğini seçin. - Not Tercihleri - Not ve diğer ayarları seçin. hakkında Bizimle uygulama hakkında daha fazla bilgi edinin. Varsayılan Not Renk @@ -190,19 +159,6 @@ Not ekle Kontrol Listesi Ekle - Blok Formatlama - başlık - Alt Başlık - Metin - Alıntı - kod - Kontrol Listesi - İşaretleme Biçimlendirme - cesur - italik yazı - Altını çizmek - Vuruş - Liste Not Seç Notlar\'ı seçin Ayarlar @@ -216,9 +172,6 @@ Ne var ne yok Uygulamanın son güncellemelerindeki yenilikleri bilin Çevirmek - Yeni bir not eklemek için tıklayın - Ev seçeneklerini görmek için tıklayın - Ana menü, favori ve arşiv notların yanı sıra uygulama ayarlarına sahiptir. Etiketlerinizi de burada bulabilirsiniz. @@ -236,25 +189,19 @@ Anlıyorum Otomatik Olarak Dışa Aktar Notları sık sık dış dosyaya yedekleme olarak dışa aktarın - görüntü Resmi eklemek veya değiştirmek için tıklayın Sadece bir kere Günlük - görenek - Beta Hatırlatıcılar ve Alarmlar %s açık kaynaklıdır ve herkes daha iyi hale getirmeye katkıda bulunmaya açıktır. Şu anda %s tarafından oluşturulmakta ve sürdürülmektedir. %s, uygulamayı çeken basit bir nottur. Kullanımı çok zor hale getirmeden hızlı Zengin Metin girişi sağlar. Çok görevli bir esinti yapar. Merhaba, biz %s \'ı yaratan bir çift tasarımcı ve programcıyız. Herkese harika bir hizmet sunan güzel, dikkatlice tasarlanmış ücretsiz, reklamsız veya minimum reklam uygulamaları oluşturmaya çalışıyoruz! %d ana ekranda çizgiler - %s ara … Distraksiyon Ücretsiz - Ayırıcı - @@ -262,27 +209,18 @@ Bulut yedekleme ve senkronizasyon için oturum açın Oturumu Kapat Bulut yedeklemeyi durdurmak için çıkış yapın - Google ile giriş yap - Oturum açma | - Oturum açtınız Google Giriş Başarısız Oldu Gizlilik Politikası İçerik için uygulama gizlilik politikası Pro Uygulamasını Yükle Neden Pro Install Bulut senkronizasyonunu çalıştırmak için büyük sunucu maliyetleri için geliştiriciyi destekleyin \n\n En yeni özellikleri edinin \n\n Bazı ekstra özellikler yalnızca Pro kullanıcıları tarafından kullanılabilir olacak - Cloud Sync\'i neden etkinleştirebilirim? - Cihaz değişikliklerine karşı yükle ve yedekle \n\n Birden fazla cihaz arasında senkronize edin ve hızlı güncellemeler yapın \n\n Güvenli bir şekilde Google Firebase sunucularına kaydedin - Merhaba, - Malzeme Notları artık Scarlet! - Başlamak Uygulama Teması Temanın arka plan rengini seçin Uygulama Temasını Seç Etiket eklemek veya değiştirmek için tıklayın - Diğer İşlemleri Göster Notlar Seç Tek seferde birden fazla notta eylemleri seçip gerçekleştirin Not çöp kutusuna taşındı @@ -322,8 +260,6 @@ Notları Birleştirme Markdown Olarak Dışa Aktar Notları işaretleme biçiminde dışa aktarın. (Bunları uygulamaya geri alamazsınız) - Klasörü Ver - Tüm ihracatın nerede saklanacağını seçin Arka plan için tema rengini kullan Arka plan için not rengini kullan etkinleştirme diff --git a/base/src/main/res/values-zh-rCN/strings.xml b/base/src/main/res/values-zh-rCN/strings.xml index 98015636..8cc0fe7e 100644 --- a/base/src/main/res/values-zh-rCN/strings.xml +++ b/base/src/main/res/values-zh-rCN/strings.xml @@ -9,18 +9,12 @@ 没有笔记 您尚未添加笔记。点击以添加一条笔记。 - 添加笔记… - 添加快速笔记… - 添加笔记 添加清单项 - 夜间模式 - 日间模式 编辑笔记 创建通知 在弹出窗口中打开 - 删除笔记 永久删除 复制笔记 发送笔记 @@ -41,8 +35,6 @@ 取消固定笔记 选择操作… - 轻触以在夜间模式下查看该笔记 - 轻触以在日间模式下查看该笔记 轻触以打开并编辑该笔记 轻触以将该笔记的内容复制到剪贴板 轻触以将该笔记的内容分享到其他应用 @@ -56,57 +48,25 @@ 轻触以更改该笔记的背景色 轻触以取消归档 - 您的笔记和其他 主页 - 您所有的普通笔记和已收藏的笔记 收藏夹 - 您所有已收藏的笔记 归档 - 您所有已归档的笔记 已锁定 - 您所有已锁定的笔记 - 标签 - 您所有的笔记标签 回收站 - 您所有在回收站中的笔记 添加内容… 添加标题… - 添加副标题… 添加引用文本… 添加列表项… 添加代码… - 块排版 - 标题 - 副标题 - 文本 - 引用 - 代码 - 清单 - - Markdown 排版 - 加粗 - 倾斜 - 下划线 - 删除线 - 列表 - 选项和设置 界面和体验 选择本应用的观感 - 笔记首选项 - 选择笔记和其他的设置 关于 了解更多关于我们和本应用的信息 安全 锁定笔记和安全方面的选项 - 启用夜间模式 - 将暗色主题设为默认主题 - 启用日间模式 - 将亮色主题设为默认主题 - Markdown 设置 - 看看如何在笔记中使用 markdown 列表视图 将笔记显示为一列 网格视图 @@ -124,8 +84,6 @@ 了解更多关于开源项目的信息 Rate and Review 告诉我们您有多喜欢本应用 - 参与调查 - 说说您喜欢什么,以帮助我们改善本应用 安全选项 密码 @@ -138,7 +96,6 @@ 输入新的 PIN 码 输入 PIN 码解锁 输入当前的 PIN 码 - 输入密码 验证 解锁 设置 @@ -152,7 +109,6 @@ 允许 markdown 支持的排版 笔记列表中的 Markdown 在笔记列表中使用 Markdown 排版 - 示例 授权 导入和导出功能需要存储权限。请在收到请求后授权。 @@ -173,7 +129,6 @@ 导出已锁定的笔记 同样备份已锁定的笔记 - 关于我们和其他 在 Play 商店上评分 贡献 应用版本 @@ -181,9 +136,7 @@ 开源项目 - 关于开源项目 - 笔记提醒 提醒日期 提醒时间 重复频率 @@ -214,9 +167,6 @@ 了解本应用最近更新的内容 翻译 - 点击新建一条笔记 - 点击查看主页选项 - 主页菜单中有您已收藏和归档的笔记,以及应用设置。您还可以在此找到您的标签。 @@ -263,51 +213,37 @@ 我明白 自动导出 频繁导出笔记到外部文件作为备份 - 图片 点击添加或更改图像 只有一次 日常 - 习惯 - Beta版 提醒和警报 %s是开源的,任何人都可以为改善它做出贡献。 它目前由%s构建和维护。 %s是一个简单的笔记应用程序。 它允许快速富文本输入,而不会让使用体验变得非常困难。 它使得多任务轻而易举。 嗨,我们是一对设计师和程序员,他们创造了%s。 我们致力于打造漂亮,精心设计的免费,无广告或最低限度的广告应用程序,这些应用程序对每个人都非常实用! 主屏幕上的%d行 - 搜索%s … 分心免费 - 分隔器 登录到应用程序 登录云备份和同步 登出 注销以停止云备份 - 用Google登录 - 登录… - 您已经登录 Google登录失败 隐私政策 应用程序隐私政策的内容 安装Pro App 为什么安装Pro 支持大规模服务器成本的开发人员,运行云同步 \n\n 首先获取最新功能 \n\n 一些额外功能仅适用于专业用户 - 为什么启用云端同步 - 根据设备更改进行上传和备份 \n\n 在多个设备之间进行同步,并获得快速更新 \n\n 安全地保存在Google Firebase服务器上 - 你好, - 材料的笔记现在是猩红色! - 开始使用 应用主题 选择主题的背景颜色 选择应用主题 单击以添加或更改标签 - 显示更多操作 选择备注 一次选择并对多个音符执行操作 这张纸条被移到了垃圾桶里 @@ -381,8 +317,6 @@ 合并备注 出口为降价 以降价格式导出备注。 (你不能将这些导回到应用程序) - 导出文件夹 - 选择存储所有导出的位置 使用主题颜色为背景 使用备注颜色作为背景 启用 diff --git a/base/src/main/res/values-zh-rHK/strings.xml b/base/src/main/res/values-zh-rHK/strings.xml index ba1065d2..9ef381ee 100644 --- a/base/src/main/res/values-zh-rHK/strings.xml +++ b/base/src/main/res/values-zh-rHK/strings.xml @@ -6,29 +6,20 @@ 分享使用… 沒有筆記 看來你還沒有添加任何筆記。 點擊添加備註。 - 添加備註… - 添加快速筆記… 編輯註釋 在彈出窗口中打開 - 刪除註釋 複製說明 發送注意事項 選擇動作… 添加內容… 添加標題… - 添加子標題… 添加引號文本… 新增項目… 添加代碼… 選項和設置 導出註釋 - 日間模式 換顏色 - 點按即可在日間模式下查看筆記 點擊改變音符的背景顏色 - 啟用夜間模式 - 默認啟用黑色主題 - 啟用日間模式 搜索筆記… 鎖定注意 解鎖注意 @@ -44,7 +35,6 @@ 輸入新的PIN碼 輸入PIN碼即可解鎖 輸入當前的PIN碼 - 輸入代碼 校驗 開鎖 @@ -54,7 +44,6 @@ 啟用網格佈局 以交錯格子顯示筆記 - 默認啟用輕量級主題 將筆記導出到設備存儲以進行共享 導入註釋 從設備存儲導入筆記 @@ -72,12 +61,10 @@ 導出到文件… 完成 分享 - 關於我們和更多 Play商店的價格 有助於 應用程式版本 關於應用 - 夜間模式 永久刪除 解除注意 存檔注意 @@ -85,7 +72,6 @@ 標記收藏 還原註意 移到廢紙簍 - 點按即可在夜間模式下查看筆記 點擊以打開註釋進行編輯 點擊將音符內容複製到剪貼板 點按即可將筆記內容分享到其他應用 @@ -97,26 +83,15 @@ 點擊標記註意不是最喜歡的 點擊歸檔筆記 點擊以取消存檔 - 你的筆記… - 所有正常和最喜歡的筆記 最愛 - 所有你最喜歡的筆記 存檔 - 所有的歸檔筆記 垃圾 - 你在垃圾桶裡的所有筆記 減價支持 允許降價支持格式 - 例子 - 減價設置 - 看看如何在筆記中使用降價 創建通知 注意通知 鎖定 - 所有鎖定的筆記 - 標籤 - 所有的筆記標籤 添加新標籤… 編輯標籤… 輸入標籤 @@ -127,8 +102,6 @@ 進口 註釋列表中的降價 在筆記列表中啟用Markdown格式 - 填寫調查 - 通過告訴我們你喜歡什麼來幫助我們改進應用程序 更改標籤 @@ -137,8 +110,6 @@ 了解更多關於開源項目 圖書館 開源項目 - 關於開源項目 - 注意提醒 提醒日期 提醒時間 重複頻率 @@ -153,8 +124,6 @@ 界面和經驗 選擇應用程序的外觀和感覺。 - 注意首選項 - 選擇筆記和其他設置。 關於 了解更多關於我們和應用程序。 默認的注意顏色 @@ -189,19 +158,6 @@ 添加註釋 添加清單 - 塊格式化 - 標題 - 小標題 - 文本 - 引用 - - 清單 - 降價格式 - 膽大 - 斜體 - 強調 - 罷工 - 名單 選擇一個註釋 選擇註釋 設置 @@ -215,9 +171,6 @@ 什麼是新的 了解最近更新的應用程序中的新功能 翻譯 - 點擊添加一個新的筆記 - 點擊查看家庭選項 - 主菜單有您最喜歡的和存檔的筆記,以及應用程序設置。 你也可以在這裡找到你的標籤。 @@ -235,51 +188,37 @@ 我明白 自動導出 頻繁導出筆記到外部文件作為備份 - 圖片 點擊添加或更改圖像 只有一次 日常 - 習慣 - Beta版 提醒和警報 %s是開源的,任何人都可以為改善它做出貢獻。 它目前由%s構建和維護。 %s是一個簡單的筆記應用程序。 它允許快速富文本輸入,而不會讓使用體驗變得非常困難。 它使得多任務輕而易舉。 嗨,我們是一對設計師和程序員,他們創造了%s。 我們致力於打造漂亮,精心設計的免費,無廣告或最低限度的廣告應用程序,這些應用程序對每個人都非常實用! 主屏幕上的%d行 - 搜索%s … 分心免費 - 分隔器 登錄到應用程序 登錄雲備份和同步 登出 註銷以停止雲備份 - 用Google登錄 - 登錄… - 您已經登錄 Google登錄失敗 隱私政策 應用程序隱私政策的內容 安裝Pro App 為什麼安裝Pro 支持大規模服務器成本的開發人員,運行云同步 \n\n 首先獲取最新功能 \n\n 一些額外功能僅適用於專業用戶 - 為什麼啟用雲端同步 - 根據設備更改進行上傳和備份 \n\n 在多個設備之間進行同步,並獲得快速更新 \n\n 安全地保存在Google Firebase服務器上 - 你好, - 材料的筆記現在是猩紅色! - 開始使用 應用主題 選擇主題的背景顏色 選擇應用主題 單擊以添加或更改標籤 - 顯示更多操作 選擇備註 一次選擇並對多個音符執行操作 這張紙條被移到了垃圾桶裡 @@ -319,8 +258,6 @@ 合併備註 出口為降價 以降價格式導出備註。 (你不能將這些導回到應用程序) - 導出文件夾 - 選擇存儲所有導出的位置 使用主題顏色為背景 使用備註顏色作為背景 啟用 diff --git a/base/src/main/res/values-zh-rMO/strings.xml b/base/src/main/res/values-zh-rMO/strings.xml index ba1065d2..9ef381ee 100644 --- a/base/src/main/res/values-zh-rMO/strings.xml +++ b/base/src/main/res/values-zh-rMO/strings.xml @@ -6,29 +6,20 @@ 分享使用… 沒有筆記 看來你還沒有添加任何筆記。 點擊添加備註。 - 添加備註… - 添加快速筆記… 編輯註釋 在彈出窗口中打開 - 刪除註釋 複製說明 發送注意事項 選擇動作… 添加內容… 添加標題… - 添加子標題… 添加引號文本… 新增項目… 添加代碼… 選項和設置 導出註釋 - 日間模式 換顏色 - 點按即可在日間模式下查看筆記 點擊改變音符的背景顏色 - 啟用夜間模式 - 默認啟用黑色主題 - 啟用日間模式 搜索筆記… 鎖定注意 解鎖注意 @@ -44,7 +35,6 @@ 輸入新的PIN碼 輸入PIN碼即可解鎖 輸入當前的PIN碼 - 輸入代碼 校驗 開鎖 @@ -54,7 +44,6 @@ 啟用網格佈局 以交錯格子顯示筆記 - 默認啟用輕量級主題 將筆記導出到設備存儲以進行共享 導入註釋 從設備存儲導入筆記 @@ -72,12 +61,10 @@ 導出到文件… 完成 分享 - 關於我們和更多 Play商店的價格 有助於 應用程式版本 關於應用 - 夜間模式 永久刪除 解除注意 存檔注意 @@ -85,7 +72,6 @@ 標記收藏 還原註意 移到廢紙簍 - 點按即可在夜間模式下查看筆記 點擊以打開註釋進行編輯 點擊將音符內容複製到剪貼板 點按即可將筆記內容分享到其他應用 @@ -97,26 +83,15 @@ 點擊標記註意不是最喜歡的 點擊歸檔筆記 點擊以取消存檔 - 你的筆記… - 所有正常和最喜歡的筆記 最愛 - 所有你最喜歡的筆記 存檔 - 所有的歸檔筆記 垃圾 - 你在垃圾桶裡的所有筆記 減價支持 允許降價支持格式 - 例子 - 減價設置 - 看看如何在筆記中使用降價 創建通知 注意通知 鎖定 - 所有鎖定的筆記 - 標籤 - 所有的筆記標籤 添加新標籤… 編輯標籤… 輸入標籤 @@ -127,8 +102,6 @@ 進口 註釋列表中的降價 在筆記列表中啟用Markdown格式 - 填寫調查 - 通過告訴我們你喜歡什麼來幫助我們改進應用程序 更改標籤 @@ -137,8 +110,6 @@ 了解更多關於開源項目 圖書館 開源項目 - 關於開源項目 - 注意提醒 提醒日期 提醒時間 重複頻率 @@ -153,8 +124,6 @@ 界面和經驗 選擇應用程序的外觀和感覺。 - 注意首選項 - 選擇筆記和其他設置。 關於 了解更多關於我們和應用程序。 默認的注意顏色 @@ -189,19 +158,6 @@ 添加註釋 添加清單 - 塊格式化 - 標題 - 小標題 - 文本 - 引用 - - 清單 - 降價格式 - 膽大 - 斜體 - 強調 - 罷工 - 名單 選擇一個註釋 選擇註釋 設置 @@ -215,9 +171,6 @@ 什麼是新的 了解最近更新的應用程序中的新功能 翻譯 - 點擊添加一個新的筆記 - 點擊查看家庭選項 - 主菜單有您最喜歡的和存檔的筆記,以及應用程序設置。 你也可以在這裡找到你的標籤。 @@ -235,51 +188,37 @@ 我明白 自動導出 頻繁導出筆記到外部文件作為備份 - 圖片 點擊添加或更改圖像 只有一次 日常 - 習慣 - Beta版 提醒和警報 %s是開源的,任何人都可以為改善它做出貢獻。 它目前由%s構建和維護。 %s是一個簡單的筆記應用程序。 它允許快速富文本輸入,而不會讓使用體驗變得非常困難。 它使得多任務輕而易舉。 嗨,我們是一對設計師和程序員,他們創造了%s。 我們致力於打造漂亮,精心設計的免費,無廣告或最低限度的廣告應用程序,這些應用程序對每個人都非常實用! 主屏幕上的%d行 - 搜索%s … 分心免費 - 分隔器 登錄到應用程序 登錄雲備份和同步 登出 註銷以停止雲備份 - 用Google登錄 - 登錄… - 您已經登錄 Google登錄失敗 隱私政策 應用程序隱私政策的內容 安裝Pro App 為什麼安裝Pro 支持大規模服務器成本的開發人員,運行云同步 \n\n 首先獲取最新功能 \n\n 一些額外功能僅適用於專業用戶 - 為什麼啟用雲端同步 - 根據設備更改進行上傳和備份 \n\n 在多個設備之間進行同步,並獲得快速更新 \n\n 安全地保存在Google Firebase服務器上 - 你好, - 材料的筆記現在是猩紅色! - 開始使用 應用主題 選擇主題的背景顏色 選擇應用主題 單擊以添加或更改標籤 - 顯示更多操作 選擇備註 一次選擇並對多個音符執行操作 這張紙條被移到了垃圾桶裡 @@ -319,8 +258,6 @@ 合併備註 出口為降價 以降價格式導出備註。 (你不能將這些導回到應用程序) - 導出文件夾 - 選擇存儲所有導出的位置 使用主題顏色為背景 使用備註顏色作為背景 啟用 diff --git a/base/src/main/res/values-zh-rTW/strings.xml b/base/src/main/res/values-zh-rTW/strings.xml index ba1065d2..9ef381ee 100644 --- a/base/src/main/res/values-zh-rTW/strings.xml +++ b/base/src/main/res/values-zh-rTW/strings.xml @@ -6,29 +6,20 @@ 分享使用… 沒有筆記 看來你還沒有添加任何筆記。 點擊添加備註。 - 添加備註… - 添加快速筆記… 編輯註釋 在彈出窗口中打開 - 刪除註釋 複製說明 發送注意事項 選擇動作… 添加內容… 添加標題… - 添加子標題… 添加引號文本… 新增項目… 添加代碼… 選項和設置 導出註釋 - 日間模式 換顏色 - 點按即可在日間模式下查看筆記 點擊改變音符的背景顏色 - 啟用夜間模式 - 默認啟用黑色主題 - 啟用日間模式 搜索筆記… 鎖定注意 解鎖注意 @@ -44,7 +35,6 @@ 輸入新的PIN碼 輸入PIN碼即可解鎖 輸入當前的PIN碼 - 輸入代碼 校驗 開鎖 @@ -54,7 +44,6 @@ 啟用網格佈局 以交錯格子顯示筆記 - 默認啟用輕量級主題 將筆記導出到設備存儲以進行共享 導入註釋 從設備存儲導入筆記 @@ -72,12 +61,10 @@ 導出到文件… 完成 分享 - 關於我們和更多 Play商店的價格 有助於 應用程式版本 關於應用 - 夜間模式 永久刪除 解除注意 存檔注意 @@ -85,7 +72,6 @@ 標記收藏 還原註意 移到廢紙簍 - 點按即可在夜間模式下查看筆記 點擊以打開註釋進行編輯 點擊將音符內容複製到剪貼板 點按即可將筆記內容分享到其他應用 @@ -97,26 +83,15 @@ 點擊標記註意不是最喜歡的 點擊歸檔筆記 點擊以取消存檔 - 你的筆記… - 所有正常和最喜歡的筆記 最愛 - 所有你最喜歡的筆記 存檔 - 所有的歸檔筆記 垃圾 - 你在垃圾桶裡的所有筆記 減價支持 允許降價支持格式 - 例子 - 減價設置 - 看看如何在筆記中使用降價 創建通知 注意通知 鎖定 - 所有鎖定的筆記 - 標籤 - 所有的筆記標籤 添加新標籤… 編輯標籤… 輸入標籤 @@ -127,8 +102,6 @@ 進口 註釋列表中的降價 在筆記列表中啟用Markdown格式 - 填寫調查 - 通過告訴我們你喜歡什麼來幫助我們改進應用程序 更改標籤 @@ -137,8 +110,6 @@ 了解更多關於開源項目 圖書館 開源項目 - 關於開源項目 - 注意提醒 提醒日期 提醒時間 重複頻率 @@ -153,8 +124,6 @@ 界面和經驗 選擇應用程序的外觀和感覺。 - 注意首選項 - 選擇筆記和其他設置。 關於 了解更多關於我們和應用程序。 默認的注意顏色 @@ -189,19 +158,6 @@ 添加註釋 添加清單 - 塊格式化 - 標題 - 小標題 - 文本 - 引用 - - 清單 - 降價格式 - 膽大 - 斜體 - 強調 - 罷工 - 名單 選擇一個註釋 選擇註釋 設置 @@ -215,9 +171,6 @@ 什麼是新的 了解最近更新的應用程序中的新功能 翻譯 - 點擊添加一個新的筆記 - 點擊查看家庭選項 - 主菜單有您最喜歡的和存檔的筆記,以及應用程序設置。 你也可以在這裡找到你的標籤。 @@ -235,51 +188,37 @@ 我明白 自動導出 頻繁導出筆記到外部文件作為備份 - 圖片 點擊添加或更改圖像 只有一次 日常 - 習慣 - Beta版 提醒和警報 %s是開源的,任何人都可以為改善它做出貢獻。 它目前由%s構建和維護。 %s是一個簡單的筆記應用程序。 它允許快速富文本輸入,而不會讓使用體驗變得非常困難。 它使得多任務輕而易舉。 嗨,我們是一對設計師和程序員,他們創造了%s。 我們致力於打造漂亮,精心設計的免費,無廣告或最低限度的廣告應用程序,這些應用程序對每個人都非常實用! 主屏幕上的%d行 - 搜索%s … 分心免費 - 分隔器 登錄到應用程序 登錄雲備份和同步 登出 註銷以停止雲備份 - 用Google登錄 - 登錄… - 您已經登錄 Google登錄失敗 隱私政策 應用程序隱私政策的內容 安裝Pro App 為什麼安裝Pro 支持大規模服務器成本的開發人員,運行云同步 \n\n 首先獲取最新功能 \n\n 一些額外功能僅適用於專業用戶 - 為什麼啟用雲端同步 - 根據設備更改進行上傳和備份 \n\n 在多個設備之間進行同步,並獲得快速更新 \n\n 安全地保存在Google Firebase服務器上 - 你好, - 材料的筆記現在是猩紅色! - 開始使用 應用主題 選擇主題的背景顏色 選擇應用主題 單擊以添加或更改標籤 - 顯示更多操作 選擇備註 一次選擇並對多個音符執行操作 這張紙條被移到了垃圾桶裡 @@ -319,8 +258,6 @@ 合併備註 出口為降價 以降價格式導出備註。 (你不能將這些導回到應用程序) - 導出文件夾 - 選擇存儲所有導出的位置 使用主題顏色為背景 使用備註顏色作為背景 啟用 diff --git a/base/src/main/res/values-zh/strings.xml b/base/src/main/res/values-zh/strings.xml index ba1065d2..9ef381ee 100644 --- a/base/src/main/res/values-zh/strings.xml +++ b/base/src/main/res/values-zh/strings.xml @@ -6,29 +6,20 @@ 分享使用… 沒有筆記 看來你還沒有添加任何筆記。 點擊添加備註。 - 添加備註… - 添加快速筆記… 編輯註釋 在彈出窗口中打開 - 刪除註釋 複製說明 發送注意事項 選擇動作… 添加內容… 添加標題… - 添加子標題… 添加引號文本… 新增項目… 添加代碼… 選項和設置 導出註釋 - 日間模式 換顏色 - 點按即可在日間模式下查看筆記 點擊改變音符的背景顏色 - 啟用夜間模式 - 默認啟用黑色主題 - 啟用日間模式 搜索筆記… 鎖定注意 解鎖注意 @@ -44,7 +35,6 @@ 輸入新的PIN碼 輸入PIN碼即可解鎖 輸入當前的PIN碼 - 輸入代碼 校驗 開鎖 @@ -54,7 +44,6 @@ 啟用網格佈局 以交錯格子顯示筆記 - 默認啟用輕量級主題 將筆記導出到設備存儲以進行共享 導入註釋 從設備存儲導入筆記 @@ -72,12 +61,10 @@ 導出到文件… 完成 分享 - 關於我們和更多 Play商店的價格 有助於 應用程式版本 關於應用 - 夜間模式 永久刪除 解除注意 存檔注意 @@ -85,7 +72,6 @@ 標記收藏 還原註意 移到廢紙簍 - 點按即可在夜間模式下查看筆記 點擊以打開註釋進行編輯 點擊將音符內容複製到剪貼板 點按即可將筆記內容分享到其他應用 @@ -97,26 +83,15 @@ 點擊標記註意不是最喜歡的 點擊歸檔筆記 點擊以取消存檔 - 你的筆記… - 所有正常和最喜歡的筆記 最愛 - 所有你最喜歡的筆記 存檔 - 所有的歸檔筆記 垃圾 - 你在垃圾桶裡的所有筆記 減價支持 允許降價支持格式 - 例子 - 減價設置 - 看看如何在筆記中使用降價 創建通知 注意通知 鎖定 - 所有鎖定的筆記 - 標籤 - 所有的筆記標籤 添加新標籤… 編輯標籤… 輸入標籤 @@ -127,8 +102,6 @@ 進口 註釋列表中的降價 在筆記列表中啟用Markdown格式 - 填寫調查 - 通過告訴我們你喜歡什麼來幫助我們改進應用程序 更改標籤 @@ -137,8 +110,6 @@ 了解更多關於開源項目 圖書館 開源項目 - 關於開源項目 - 注意提醒 提醒日期 提醒時間 重複頻率 @@ -153,8 +124,6 @@ 界面和經驗 選擇應用程序的外觀和感覺。 - 注意首選項 - 選擇筆記和其他設置。 關於 了解更多關於我們和應用程序。 默認的注意顏色 @@ -189,19 +158,6 @@ 添加註釋 添加清單 - 塊格式化 - 標題 - 小標題 - 文本 - 引用 - - 清單 - 降價格式 - 膽大 - 斜體 - 強調 - 罷工 - 名單 選擇一個註釋 選擇註釋 設置 @@ -215,9 +171,6 @@ 什麼是新的 了解最近更新的應用程序中的新功能 翻譯 - 點擊添加一個新的筆記 - 點擊查看家庭選項 - 主菜單有您最喜歡的和存檔的筆記,以及應用程序設置。 你也可以在這裡找到你的標籤。 @@ -235,51 +188,37 @@ 我明白 自動導出 頻繁導出筆記到外部文件作為備份 - 圖片 點擊添加或更改圖像 只有一次 日常 - 習慣 - Beta版 提醒和警報 %s是開源的,任何人都可以為改善它做出貢獻。 它目前由%s構建和維護。 %s是一個簡單的筆記應用程序。 它允許快速富文本輸入,而不會讓使用體驗變得非常困難。 它使得多任務輕而易舉。 嗨,我們是一對設計師和程序員,他們創造了%s。 我們致力於打造漂亮,精心設計的免費,無廣告或最低限度的廣告應用程序,這些應用程序對每個人都非常實用! 主屏幕上的%d行 - 搜索%s … 分心免費 - 分隔器 登錄到應用程序 登錄雲備份和同步 登出 註銷以停止雲備份 - 用Google登錄 - 登錄… - 您已經登錄 Google登錄失敗 隱私政策 應用程序隱私政策的內容 安裝Pro App 為什麼安裝Pro 支持大規模服務器成本的開發人員,運行云同步 \n\n 首先獲取最新功能 \n\n 一些額外功能僅適用於專業用戶 - 為什麼啟用雲端同步 - 根據設備更改進行上傳和備份 \n\n 在多個設備之間進行同步,並獲得快速更新 \n\n 安全地保存在Google Firebase服務器上 - 你好, - 材料的筆記現在是猩紅色! - 開始使用 應用主題 選擇主題的背景顏色 選擇應用主題 單擊以添加或更改標籤 - 顯示更多操作 選擇備註 一次選擇並對多個音符執行操作 這張紙條被移到了垃圾桶裡 @@ -319,8 +258,6 @@ 合併備註 出口為降價 以降價格式導出備註。 (你不能將這些導回到應用程序) - 導出文件夾 - 選擇存儲所有導出的位置 使用主題顏色為背景 使用備註顏色作為背景 啟用 diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index ee301e6a..0d32ebb2 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -6,24 +6,17 @@ Save Import Search Notes… - Search %s… Share using… No Notes It seems you have not added any notes. Click to add a note. - Add a note… - Add a quick note… - Add Note Add Checklist - Night Mode - Day Mode Edit Note Create Notification Open In Popup - Delete Note Delete Permanently Copy Note Send Note @@ -46,8 +39,6 @@ Merge Notes Choose Action… - Tap to view the note in night mode - Tap to view the note in day mode Tap to open note for editing Tap to copy note content to clipboard Tap to share note content to other apps @@ -61,60 +52,26 @@ Tap to change the background colour of the note Tap to unarchive the note - Your Notes and More… Home - All of your normal and favourite notes Favourites - All of your favourite notes Archived - All of your archived notes Locked - All of your locked notes - Tags - All your note tags Trash - All of your notes in the trash Add Content… Add Heading… - Add Sub Heading… Add Quoted Text… Add Item… Add Code… Click to add or change image - Block Formatting - Heading - Sub Heading - Text - Quote - Code - Checklist - Image - Separator - - Markdown Formatting - Bold - Italics - Underline - Strike - List - Options and Settings Interface and Experience Choose how the app looks and feels. - Note Preferences - Choose note and other settings. About Know more about us and the app. Security Locking notes and security options - Enable Night Mode - Enable dark theme as default - Enable Day Mode - Enable light theme as default - Markdown Settings - See how to use markdown in notes Enable List Layout Show notes in a single column Enable Grid Layout @@ -128,8 +85,6 @@ Import notes from the device storage Export As Markdown Export notes in markdown format. (You cannot import these back to the app) - Export Folder - Choose where all exports are stored Export Automatically Export notes frequently to external file as backup About Us @@ -138,8 +93,6 @@ Know more about the open source project Rate and Review Tell us how much you liked the app - Fill Survey - Help us improve the app by telling us what you like Install Pro App Install the Pro App to unlock features and support developer Migrate Notes to Scarlet Pro @@ -174,7 +127,6 @@ Enter new PIN Enter PIN to unlock Enter current PIN - enter code Verify Unlock Set @@ -189,7 +141,6 @@ Allow markdown supported formatting Markdown in Note List Enable Markdown formatting in the list of notes - Examples # Heading \n## Subheading \n\n```\nblock of code\n```\n\n> quote\n\n- **bold**\n- *italics*\n- _underline_\n- ~~strike~~\n- `code`\n Give Permissions @@ -218,7 +169,6 @@ %s MB %s GB - About Us and More Rate on Play Store Contribute App Version @@ -226,16 +176,12 @@ Libraries Open Source Project - About Open Source Project - Note Reminder Reminder Date Reminder Time Repeat Frequency Only Once Daily - Custom - Beta Note Notifications Reminders and Alarms @@ -274,10 +220,6 @@ Know what is new in the recent updates of the app Translate - Click to add a new note - Click to see home options - The home menu has your favourite and archive notes, as well as application settings. You can find your tags here too. - Copy Block Text Actions Gallery @@ -332,9 +274,6 @@ Sign In for cloud backup and synchronisation Sign Out Sign Out to stop cloud backup - Sign In with Google - Signing in… - You are logged in Google Login Failed Privacy Policy @@ -347,20 +286,13 @@ ✔ Support developer for the massive server costs, to run the cloud sync \n\n✔ Get the latest features first \n\n✔ Some extra features will be available only to Pro users - Why enable Cloud Sync - - ✔ Upload and backup against device changes \n\n✔ Sync between multiple devices, and get quick updates \n\n✔ Securely saved on Google Firebase servers - Forget Me Logout and remove all online data - Hello, Scarlet - Material Notes is now Scarlet! - Get Started @@ -371,7 +303,6 @@ Click to add or change tags - Show More Actions Select Notes Select and perform actions on multiple notes at once From c4c23401e0a5f3dc049fa803a74d8850139f375f Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 30 Jul 2019 00:32:20 +0100 Subject: [PATCH 053/134] Removing redundant images from resources --- app/build.gradle | 4 ++-- .../res/drawable-hdpi/ic_action_enabled.png | Bin 696 -> 0 bytes .../res/drawable-hdpi/ic_action_reminder.png | Bin 1058 -> 0 bytes .../main/res/drawable-hdpi/ic_action_search.png | Bin 626 -> 0 bytes .../main/res/drawable-hdpi/ic_add_folder.png | Bin 375 -> 0 bytes .../main/res/drawable-hdpi/ic_add_search.png | Bin 635 -> 0 bytes .../res/drawable-hdpi/ic_add_white_48dp.png | Bin 97 -> 0 bytes .../ic_arrow_upward_white_48dp.png | Bin 319 -> 0 bytes .../ic_border_color_white_24dp.png | Bin 200 -> 0 bytes .../main/res/drawable-hdpi/ic_chevron_left.png | Bin 222 -> 0 bytes .../main/res/drawable-hdpi/ic_chevron_right.png | Bin 232 -> 0 bytes .../drawable-hdpi/ic_drag_handle_white_48dp.png | Bin 94 -> 0 bytes .../ic_fiber_smart_record_white_24dp.png | Bin 421 -> 0 bytes .../ic_fiber_smart_record_white_48dp.png | Bin 750 -> 0 bytes .../ic_format_list_numbered_white_48dp.png | Bin 229 -> 0 bytes .../ic_keyboard_arrow_down_white_48dp.png | Bin 268 -> 0 bytes .../ic_keyboard_arrow_up_white_48dp.png | Bin 259 -> 0 bytes .../drawable-hdpi/ic_more_vert_white_48dp.png | Bin 216 -> 0 bytes .../ic_playlist_add_check_white_48dp.png | Bin 236 -> 0 bytes base/src/main/res/drawable-hdpi/ic_refresh.png | Bin 666 -> 0 bytes .../res/drawable-hdpi/ic_reorder_white_48dp.png | Bin 96 -> 0 bytes .../drawable-hdpi/ic_settings_white_48dp.png | Bin 843 -> 0 bytes .../drawable-hdpi/ic_text_fields_white_48dp.png | Bin 134 -> 0 bytes .../res/drawable-mdpi/ic_action_enabled.png | Bin 449 -> 0 bytes .../res/drawable-mdpi/ic_action_reminder.png | Bin 720 -> 0 bytes .../main/res/drawable-mdpi/ic_action_search.png | Bin 427 -> 0 bytes .../main/res/drawable-mdpi/ic_add_folder.png | Bin 266 -> 0 bytes .../main/res/drawable-mdpi/ic_add_search.png | Bin 418 -> 0 bytes .../res/drawable-mdpi/ic_add_white_48dp.png | Bin 97 -> 0 bytes .../ic_arrow_upward_white_48dp.png | Bin 217 -> 0 bytes .../ic_border_color_white_24dp.png | Bin 159 -> 0 bytes .../main/res/drawable-mdpi/ic_chevron_left.png | Bin 163 -> 0 bytes .../main/res/drawable-mdpi/ic_chevron_right.png | Bin 162 -> 0 bytes .../drawable-mdpi/ic_drag_handle_white_48dp.png | Bin 91 -> 0 bytes .../ic_fiber_smart_record_white_24dp.png | Bin 263 -> 0 bytes .../ic_fiber_smart_record_white_48dp.png | Bin 517 -> 0 bytes .../ic_format_list_numbered_white_48dp.png | Bin 177 -> 0 bytes .../ic_keyboard_arrow_down_white_48dp.png | Bin 201 -> 0 bytes .../ic_keyboard_arrow_up_white_48dp.png | Bin 179 -> 0 bytes .../drawable-mdpi/ic_more_vert_white_48dp.png | Bin 158 -> 0 bytes .../ic_playlist_add_check_white_48dp.png | Bin 163 -> 0 bytes base/src/main/res/drawable-mdpi/ic_refresh.png | Bin 421 -> 0 bytes .../res/drawable-mdpi/ic_reorder_white_48dp.png | Bin 93 -> 0 bytes .../drawable-mdpi/ic_settings_white_48dp.png | Bin 562 -> 0 bytes .../drawable-mdpi/ic_text_fields_white_48dp.png | Bin 105 -> 0 bytes .../res/drawable-xhdpi/ic_action_enabled.png | Bin 830 -> 0 bytes .../res/drawable-xhdpi/ic_action_reminder.png | Bin 1364 -> 0 bytes .../res/drawable-xhdpi/ic_action_search.png | Bin 818 -> 0 bytes .../main/res/drawable-xhdpi/ic_add_folder.png | Bin 477 -> 0 bytes .../main/res/drawable-xhdpi/ic_add_search.png | Bin 866 -> 0 bytes .../res/drawable-xhdpi/ic_add_white_48dp.png | Bin 102 -> 0 bytes .../ic_arrow_upward_white_48dp.png | Bin 354 -> 0 bytes .../ic_border_color_white_24dp.png | Bin 223 -> 0 bytes .../main/res/drawable-xhdpi/ic_chevron_left.png | Bin 220 -> 0 bytes .../res/drawable-xhdpi/ic_chevron_right.png | Bin 222 -> 0 bytes .../ic_drag_handle_white_48dp.png | Bin 95 -> 0 bytes .../ic_fiber_smart_record_white_24dp.png | Bin 517 -> 0 bytes .../ic_fiber_smart_record_white_48dp.png | Bin 999 -> 0 bytes .../ic_format_list_numbered_white_48dp.png | Bin 277 -> 0 bytes .../ic_keyboard_arrow_down_white_48dp.png | Bin 303 -> 0 bytes .../ic_keyboard_arrow_up_white_48dp.png | Bin 284 -> 0 bytes .../drawable-xhdpi/ic_more_vert_white_48dp.png | Bin 305 -> 0 bytes .../ic_playlist_add_check_white_48dp.png | Bin 289 -> 0 bytes base/src/main/res/drawable-xhdpi/ic_refresh.png | Bin 804 -> 0 bytes .../drawable-xhdpi/ic_reorder_white_48dp.png | Bin 99 -> 0 bytes .../drawable-xhdpi/ic_settings_white_48dp.png | Bin 1074 -> 0 bytes .../ic_text_fields_white_48dp.png | Bin 111 -> 0 bytes .../res/drawable-xxhdpi/ic_action_enabled.png | Bin 1432 -> 0 bytes .../res/drawable-xxhdpi/ic_action_reminder.png | Bin 2012 -> 0 bytes .../res/drawable-xxhdpi/ic_action_search.png | Bin 1333 -> 0 bytes .../main/res/drawable-xxhdpi/ic_add_folder.png | Bin 763 -> 0 bytes .../main/res/drawable-xxhdpi/ic_add_search.png | Bin 1439 -> 0 bytes .../res/drawable-xxhdpi/ic_add_white_48dp.png | Bin 113 -> 0 bytes .../ic_arrow_upward_white_48dp.png | Bin 506 -> 0 bytes .../ic_border_color_white_24dp.png | Bin 292 -> 0 bytes .../res/drawable-xxhdpi/ic_chevron_left.png | Bin 376 -> 0 bytes .../res/drawable-xxhdpi/ic_chevron_right.png | Bin 389 -> 0 bytes .../ic_drag_handle_white_48dp.png | Bin 104 -> 0 bytes .../ic_fiber_smart_record_white_24dp.png | Bin 750 -> 0 bytes .../ic_fiber_smart_record_white_48dp.png | Bin 1491 -> 0 bytes .../ic_format_list_numbered_white_48dp.png | Bin 373 -> 0 bytes .../ic_keyboard_arrow_down_white_48dp.png | Bin 392 -> 0 bytes .../ic_keyboard_arrow_up_white_48dp.png | Bin 371 -> 0 bytes .../drawable-xxhdpi/ic_more_vert_white_48dp.png | Bin 472 -> 0 bytes .../ic_playlist_add_check_white_48dp.png | Bin 381 -> 0 bytes .../src/main/res/drawable-xxhdpi/ic_refresh.png | Bin 1357 -> 0 bytes .../drawable-xxhdpi/ic_reorder_white_48dp.png | Bin 110 -> 0 bytes .../drawable-xxhdpi/ic_settings_white_48dp.png | Bin 1606 -> 0 bytes .../ic_text_fields_white_48dp.png | Bin 124 -> 0 bytes .../res/drawable-xxxhdpi/ic_add_white_48dp.png | Bin 116 -> 0 bytes .../ic_arrow_upward_white_48dp.png | Bin 620 -> 0 bytes .../ic_border_color_white_24dp.png | Bin 346 -> 0 bytes .../ic_drag_handle_white_48dp.png | Bin 109 -> 0 bytes .../ic_fiber_smart_record_white_24dp.png | Bin 999 -> 0 bytes .../ic_fiber_smart_record_white_48dp.png | Bin 1968 -> 0 bytes .../ic_format_list_numbered_white_48dp.png | Bin 490 -> 0 bytes .../ic_keyboard_arrow_down_white_48dp.png | Bin 452 -> 0 bytes .../ic_keyboard_arrow_up_white_48dp.png | Bin 444 -> 0 bytes .../ic_more_vert_white_48dp.png | Bin 641 -> 0 bytes .../ic_playlist_add_check_white_48dp.png | Bin 533 -> 0 bytes .../drawable-xxxhdpi/ic_reorder_white_48dp.png | Bin 115 -> 0 bytes .../drawable-xxxhdpi/ic_settings_white_48dp.png | Bin 2248 -> 0 bytes .../ic_text_fields_white_48dp.png | Bin 133 -> 0 bytes base/src/main/res/drawable/bullet_1.png | Bin 723 -> 0 bytes base/src/main/res/drawable/bullet_2.png | Bin 1106 -> 0 bytes base/src/main/res/drawable/bullet_3.png | Bin 228 -> 0 bytes .../res/drawable/get_started_background.xml | 14 -------------- base/src/main/res/drawable/ic_scarlet_logo.png | Bin 20225 -> 0 bytes .../src/main/res/drawable/image_placeholder.jpg | Bin 21330 -> 0 bytes .../res/drawable/install_pro_background.xml | 15 --------------- base/src/main/res/drawable/quote_end.png | Bin 157 -> 0 bytes base/src/main/res/drawable/quote_start.png | Bin 368 -> 0 bytes build.gradle | 4 ++-- 113 files changed, 4 insertions(+), 33 deletions(-) delete mode 100644 base/src/main/res/drawable-hdpi/ic_action_enabled.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_action_reminder.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_action_search.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_add_folder.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_add_search.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_add_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_arrow_upward_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_border_color_white_24dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_chevron_left.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_chevron_right.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_drag_handle_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_fiber_smart_record_white_24dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_fiber_smart_record_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_format_list_numbered_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_keyboard_arrow_down_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_keyboard_arrow_up_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_more_vert_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_playlist_add_check_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_refresh.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_reorder_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_settings_white_48dp.png delete mode 100644 base/src/main/res/drawable-hdpi/ic_text_fields_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_action_enabled.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_action_reminder.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_action_search.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_add_folder.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_add_search.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_add_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_arrow_upward_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_border_color_white_24dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_chevron_left.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_chevron_right.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_drag_handle_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_fiber_smart_record_white_24dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_fiber_smart_record_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_format_list_numbered_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_keyboard_arrow_down_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_keyboard_arrow_up_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_more_vert_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_playlist_add_check_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_refresh.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_reorder_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_settings_white_48dp.png delete mode 100644 base/src/main/res/drawable-mdpi/ic_text_fields_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_action_enabled.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_action_reminder.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_action_search.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_add_folder.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_add_search.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_add_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_arrow_upward_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_border_color_white_24dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_chevron_left.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_chevron_right.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_drag_handle_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_fiber_smart_record_white_24dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_fiber_smart_record_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_format_list_numbered_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_keyboard_arrow_down_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_keyboard_arrow_up_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_more_vert_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_refresh.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_reorder_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_settings_white_48dp.png delete mode 100644 base/src/main/res/drawable-xhdpi/ic_text_fields_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_action_enabled.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_action_reminder.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_action_search.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_add_folder.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_add_search.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_arrow_upward_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_border_color_white_24dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_chevron_left.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_chevron_right.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_drag_handle_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_fiber_smart_record_white_24dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_fiber_smart_record_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_format_list_numbered_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_keyboard_arrow_down_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_keyboard_arrow_up_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_more_vert_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_refresh.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_reorder_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_settings_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxhdpi/ic_text_fields_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_arrow_upward_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_border_color_white_24dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_drag_handle_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_fiber_smart_record_white_24dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_fiber_smart_record_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_format_list_numbered_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_keyboard_arrow_down_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_keyboard_arrow_up_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_more_vert_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_reorder_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_settings_white_48dp.png delete mode 100644 base/src/main/res/drawable-xxxhdpi/ic_text_fields_white_48dp.png delete mode 100644 base/src/main/res/drawable/bullet_1.png delete mode 100644 base/src/main/res/drawable/bullet_2.png delete mode 100644 base/src/main/res/drawable/bullet_3.png delete mode 100644 base/src/main/res/drawable/get_started_background.xml delete mode 100644 base/src/main/res/drawable/ic_scarlet_logo.png delete mode 100644 base/src/main/res/drawable/image_placeholder.jpg delete mode 100644 base/src/main/res/drawable/install_pro_background.xml delete mode 100644 base/src/main/res/drawable/quote_end.png delete mode 100644 base/src/main/res/drawable/quote_start.png diff --git a/app/build.gradle b/app/build.gradle index 44f3f2ec..7693e57f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 136 - versionName '7.0.6' + versionCode 137 + versionName '7.0.7' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/res/drawable-hdpi/ic_action_enabled.png b/base/src/main/res/drawable-hdpi/ic_action_enabled.png deleted file mode 100644 index 53923459874089b387d8da9e60d9dedd15bff5fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmV;p0!RIcP)^{Xm<_^T?a^ z9lr6u5mPNb4cuxJ&e8=^Do5U=c-llvg@N-B#5=Tuia*qxd67AB;8%Q3(`w_zJ<#%4 zoiesZhlt+-*6`&PEK2Y|378rbze)8?Oq{yB>6;j+yUe|Y%(6KMLp=6h z_gFYygqBsPv}Zat%*gYe+Z8cTvnVz!6%hkni(tzTcr*|j8ge{M@(iRn25Phc%0y3dY@`g32)%P6 zRC9U8D^99n!|sc?5h5M*mrUZS%X4g;RK`Yp5@RPfOB(Tt6WNSCXowBXwTOXZfrh-| zw8asx;#?v0BHI^i%r#CLVk5nkNB~0#6My2QI5rNi!e?ZtcqY;CpExOwjYCuBJg@`I zBu!K`#l|_%6lG6k7m}SgajI+B1a|zKiSxjR4-=}zlf7Bi@6E(f$k!^Q?{d()q00~r zbRt0uE~|GTh$q*XaaH%D8-`5~@oCjJ)Cr8!Tyy5nm5n@SVt;5_Z4OO+rOD%aVx~a& z6h}Q5#<#K^+;$9o2p`}DLCJ=%l?mL%9^-kUk0-kcGRE7EF21ezwB}rap*1|7rtqS` eUa_n4-^(wvCnX z!21PIYijbx0P}*7mu@RX=OygYrxFko_~!u4ez*2AASsaF#q%Icq#baZl#2s?65{l% z56}kK$i}V)3<+`;02h@_6%P{NjzEaYT(P_b{N?~!0M7x7LVqV<-zeY+a@cn3ngb6m zO0L(*SI|M?BxpvhwtfcyGYXS-0-Dc$8!zr+LgW zbtJ0_@Xq!Q=Tz<0j1H5KK|El#$-fnZ{sHs=wkVvjYCx}pB5wf2)Nd_dgk&62J;-}3 z7f7o(Aqaa9*y;0r`6%#?+01Db?x^3ppj!|!%v~uPW0_B$hcIG9;O{hbksTCV+fX{U zMsD~!T}E*?7f6obvEXX=`JH@@VE6dH&_mFdu-&M+9%QW%%qiw}le>`~7d%ak$Ea0F z0UEbUaL-3|C!a-b%}&;>5OymoKx2EE%&1n>OMW=XR=+9$1*!$Pl{yM^u$6eKnE2=_ zfII(!$!^n8pr4VP2|@uHe}T#F(@|iWk<2VlEGIT`k&XI@sqj{R#G=|HRVm7a-Q#I(yN=MU%nmUt z+U!FKC$owQau=AQn{);8EbO|W`l@ACIJv#-{Vkyyk)hVxcUnF0j@X*%^_6kBlG`i%P985|TAF%Fe z;(MEJ;p+r#eNwTUK2D#n!lD16!9=#<%}T|97?l*~Y~{side`B#JoB#=%CU;OCNI5A c%QVC7FQv!j*?2Vz@c;k-07*qoM6N<$g8kS7xBvhE diff --git a/base/src/main/res/drawable-hdpi/ic_action_search.png b/base/src/main/res/drawable-hdpi/ic_action_search.png deleted file mode 100644 index 407fb1be686e9a9c0a660cb43b7db3b4b88ef691..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 626 zcmV-&0*(ENP)OFc$-0q4w=sUQZh@eNs1>C@c2hpG=?-c)n3XDaM ziIeG0M!Mi5!(^tqtNZWns&O1^)~s0zo#pfSTF?&?-~c>=H|Bxw#X&EqskY9KgA~Yu zToLG*Loiwm9glz{_$r~}5_)F?)K{wW?clUXCqJ0qo=Jn&N{sxehdfB@v!DlbFmyc+ zjyz;W@{5=ZAgSoYHJJEqfzp2<3T_qeubb-pq(bw@e~|&pFRhrp5ffYGiD>xBkA_)V zH^SGE)-4v!2Ze^|daMk8QN_;Gr!#<6(co^mBgXP``;6-hASD``4#4M}@Ohv!fM?NI zcK|+ngwH!#Yq|pXbTyO-;s7EaVpn_7b~^uu0G`DTq(egYIwh3Wu!v_(E@HWj?gX%n z9txicodFCh4xW)R{Edr)k^BwlP9&@fIW|b=AEC37%TYEP$xJG zprp*WeqTY%WDt#D%SS<9n5`rqWFO*w%yf+$<=rV;;@60iLLI?_kWoArGy46A>s>l0 z*7(GJVoAL6HKpP!V94Dues5V3eAajnBA_~6l#yb&jVlP1%wkSh%wr~U6`ctmGORv1 z`3%C!sjB4^mRV5Ai2_)3!ep+QAQqgk8PCL0=tf=!;aIa~%~~kz8wsa)QfSw-HUIzs M07*qoM6N<$f)ctPRq9AKpRVcM9LHa!9u}0;72UP%0Rj+0zvOTo$ta4 zOB(NPm)vZe7oJ9BW*)eK-AyDo91e%W;Si7Kd1;UXj{$g~Hhc-pfCJncoWUMgREE!h zO@P6F^9}kS8MnX_I7JY+N(|x_==z_9t01w7z!D6V_X!1h3d8GQ3Q{Hu{D6wd0)n3D z0=9~PrFoc@_#{9be3~fGk!nv&6lhAdM=Wp;0=DA976bN4f*SauB=8)e-wP#yw=icJ z{5KDFlmx^fa1G`XRjGDc`hDTwBS`Il@k{(K1zJ+=v55k&APWlM-9!Pg*avV7anmCu z@aM0)+jEP)@#6O^K$6?l;sF03e)phCHqfiKA7g`jp3f~G5k zg+>LvsHk9mr}aN@1`fkxJdQZC56*!fxH5C*{AbRYIlG?c%$YN1&Yay=%jI$fpb_+e zDX>6Hf*w$x&w;mtC2$1JFX0f(fo2fNm*9)QI5_!F;3`67zYv}e!KcAL7M%F|7wz;U zrII`uKJF9@Srb8N^I8e&D|im>`izf*B>2O|pMh>$0$P={$%487^E$u}Ht##Av9bHh zEQoL5;T7h0DdOK4Eo!<&j#qwQb3w5oYC|V zrMi)RUYZn;VGB=d`X~ge%+H8P0gG(mx0*igfDg=1%A|m4w(zp1k8<#t`RO+);3<2+ z#0y)k;iH2dGWnpjnG}!!2kaV6T{8`LL51nZ?7h-ub_JXl0ljSZ#Rep8v=R;AuhJ4c z3NY^(J3ZMyRoM_Ack)zeliz_BP)aR_piDeeV4m%~%>BnU+i7u^4=X{*R(TVw&|Sad zGvYR4&p1>1@U@i`u*UosL@FI>c?)sxUnzpPI>#lj@w9u+bd z@kyLem_*e3h*$%~VLOSe@?O+*(^nM6-&v!SQYD@FVdQ&MBb@07vc`*Z=?k diff --git a/base/src/main/res/drawable-hdpi/ic_arrow_upward_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_arrow_upward_white_48dp.png deleted file mode 100644 index d7b27da82ec7793d6f4e24fdd3e321e6af0ffd25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXoKNz{uw5;uuoF`1XpsADf{>bE13$ zt9O;PdTQR!%ZYwTU*0x#h^THma%{c+2M^5^L0+0mkMv4Elz4ID*eRz?g)u6^$K5v- z%uz9RS3fCZ?)l`0r_ynMA%&t2q~ObWADe^j#RfOOsY$zc}hM;KHlfMuUq=pU7A zaG7*Yvhl!e$xA%Xr${ubTw-~#)8h03rMEfNm-^ONOmNum*?()P-G`O{N8Xo?Ro{5s zSsTmmRIabzopr0I`LC!~g&Q diff --git a/base/src/main/res/drawable-hdpi/ic_border_color_white_24dp.png b/base/src/main/res/drawable-hdpi/ic_border_color_white_24dp.png deleted file mode 100644 index 79aa203d21faa3159ea537e3b18ecba03e1aba36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8zo(01h)3t!bG!K*90Xbu<9uE2 zC{?{*axi+;-C4=3iDF03%i&xx%({hQ!3xufK##H~PJ3@q2Ei`#k9F?j1L$t`DtWz2)`pU@?$o4~&e{+;Vda R$~NeLxSpS;3!Xe|e9{twf*}hKBtoE$8c86bfOV@@cjD6i2V8 z@8fDduL)95Ox2%OY5u_a(L$3)Su>@SOG-WouAS_lUgD&lI5%&aYwU8PN4l#oAFS=T zJHP3E^RM15E2r2;$Mt^lT=?gPhW_HSKOUc4%zJBr?{em*C1zLUJDV6*{A;!c^1E-Qe`Bi!xM#Y5Lcuh4T;Yyt6bDKi(JiWBZH!6jM?MiM-h#Uc%VuALuN+zRC>@^R3(K_U^ zR>`c0J;wM9mWMReom0HiNzhQ!GFfm^)$lVdTvN0wnuTu+^Frd1rXfF#99Fo48m2=n z_NiBR@SpCk{O*|IpLFnd(}Ykol?=AoIqzh=bJivURSGG3>gV6^8~E!hY*l%*urBZA P00000NkvXXu0mjfLK4L! diff --git a/base/src/main/res/drawable-hdpi/ic_fiber_smart_record_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_fiber_smart_record_white_48dp.png deleted file mode 100644 index 5466fe63ce9948c5957267c3dc48e1f7df88d16e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 750 zcmVPF zp-?Dvv7n0YsAoNUILaV{9AyvdsN-8IB`wM&I9$sZI!UF2(Oe@b;L*Y;u8@3j1N9`x zM4VcxC5h8%7EmF%qKxUp$wrK+l!^i)!emM$d)!SQx#;CC@xj)?osu0sCrYlOe5y3a z7U2WQ9RJ~w50Agag5QXhCKdiA2v&8x$Pz~MW~ZMlk05w_A{Opt1GW)9NWYY$1SR5M zy8J~#PotuYK7!QaKLmJ!1Ax();t*guL7S}3!T~jnaw^9OT8t|3;c;u?qEw28I$W49 z3jPF)8vP1Q;6hx#c3Eeoxze9t46ghy3hrSTW3)d(C$6-KibWW$VxtO|%0uE__}UKD<5G>NXvA1xht}iL8d32B#yUH+2bXq>iia_F+M%PkbXZhejd9ct z4dT*(s3^f0JQo^NYBlH_n6hgyj-CsP`jO;|dfmCU?d#6U!Pu1iVEh)BzRck2_{t7d z;?i}Z;CT#>ack9L9hNk_@Hv5~MCll$Z1o3}~ZRlH)F-1mz^R zi-iJ4Gmt!uicj$f!U!Mv-iO<;4fC#~$KM3u539`AScQkp<5yAWP4dXc2!DtLzhQon z%<&14+}DmZS$cy`@RnqUJLw@8UD`6m*21llJxZBOjBG@iNQo${u^CITEvz3^P)h4q`l#z9hK3kz2e2hdKBZ5lSw*q{T71^GAR19H%QWN_o0Z7KR@V_a&#-sz>Rgr<+|Vqz$W6hXHEyeE@b^+ z4?xQ-B8V*j2kM9gjfOjLV)@=s7(VFgU>sSHBb312MmfmvY?mR fIpoGDNz(8h3}SuY3Z$s<00000NkvXXu0mjfrG;SQ diff --git a/base/src/main/res/drawable-hdpi/ic_keyboard_arrow_down_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_keyboard_arrow_down_white_48dp.png deleted file mode 100644 index f9622b7bee7a9d6f1b8ffcc8acada4b620896452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 268 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawE_=E-hGg7(d(K;^$v~hbaoVfO zlJ%c#*1glQojxI!TP%vrNN~-O#EbvUUe40@lt}I|JO(6o9%j2ezs!0~Qo*$y5BvVU ziS1&W{$Ppv^3}%@ujyr5#wL2|KJN-Q?VDU)d7jhvkmP({mB=~kf-M4AjTM&0n;&gR z4q#=r^yr;0GE;ei5&v9y%49Hm^x9LRj-*yaMdZBs-$gd2Z Lu6{1-oD!MU#L=X%DU+^-|_uBj6@R~ZHuNXXC{an^LB{Ts5 Dk$r3v diff --git a/base/src/main/res/drawable-hdpi/ic_more_vert_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_more_vert_white_48dp.png deleted file mode 100644 index d32281307232fefc363e19d9d2728af62b440a8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw=6kw0hGg7(d(E5ckb#KX!!teA z8+IJIbSd_mp~lgrEob_e9cxnR1kZ18I^5b~uj)DJpG>-%nby>)6J_QWKRvd(S?`Ri~_DWys(>8rgyQv zG%aCaz>Js9>y=L)eUVgRxJx3oN6$@sv5NTO?u&4dNh*K$SbOB%sZae;9uIUXgQu&X J%Q~loCIECSTxI|O diff --git a/base/src/main/res/drawable-hdpi/ic_playlist_add_check_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_playlist_add_check_white_48dp.png deleted file mode 100644 index 290088718fb6f86b6efbd14b59a6f5b204140477..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHha1_hEy=Vy?Q;d)j@>$qI;^_H=VWYVN=Z2sx$0t6xc;;bA33QCXknORoMyCu(hNhPz&wYYE2lxG&2=TA=V zDQ04M{^skoB`>4TMLe3r@bk>_&vP~_-0z6=ooO8ZtJLDS2W#J~Xosw)x^`3Ate$TY zExP1+?&e8uW9`O9YNzp7H#O$LiuHmFFy%1RjfcIQC8K kne4xJ8-u!ppx{Mmr`EGqYws>B13I6<)78&qol`;+0EvQPkpKVy diff --git a/base/src/main/res/drawable-hdpi/ic_refresh.png b/base/src/main/res/drawable-hdpi/ic_refresh.png deleted file mode 100644 index d035583686fcd6239d0a8a9dd76c8b8c3213f0b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 666 zcmV;L0%iS)P)Ae4v>9f~>xQ61_c1RlKk2Sk~aLPQ??QRLlYe}F`G@e~wPQXR6y zgZ@wUHfCXY&91ZVOuG{fd~V}+-goEin>VV*GamnV34FcEWAI+@Ik0@nuV6c7>IvKpY( zbQ&^G0ZJ4TdEUX#17E;_)pTN*t59~L!?nbM*aZ<*pX$C96O41Cly{=cEBGn$^!qv! zs{&FqO*!mBJcRd<@I5doz<{4pViP=sFOg_YU=^#NwTe7M?~v$*Km$8yt-4)7FYty$ zrv)0ALu<9{3I>5sB!r2WIbGoe0E!dcO*I^(7-I$6e6MxB)Tck zz&2XzhKJ}C5-kWcaEjI{@eoZS(FZ{XnU835Kgc4axzY=#1Q=LHcM0_c4)YSPA_I?V zkHq-d3`-kM3dRZ7^wEi{SHdnbQDZq1$WygqB1f9XIPWb!r(8VFhMCZHuo(-bdrd@z z2m?)uiJDnDp}85>Tb-yfw*b6SOsMZfidLP_yq+6OtQeTkN^AkxQgfKn+gM5`(L2$! z`DK>lI}s)?c6USr>P{RwYC>0HtCYiz#ejQ$Ln9}4lJrgu(#5u=2;QWg`wfE6dSn^2 z;2qEV5e@XD{M}f}w4pR}a-|J2Am`r=#)ENu1J?1*n|iX;2><{907*qoM6N<$g3Q1g AlmGw# diff --git a/base/src/main/res/drawable-hdpi/ic_reorder_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_reorder_white_48dp.png deleted file mode 100644 index a0d2543f250627756d7457c4b6248a9cce9cb72c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJd6J$kjv*C{ r$r4Kz{IloyqdeDDz1sb)>>LIL+k3Me`&$)jL0Uas{an^LB{Ts53DX%= diff --git a/base/src/main/res/drawable-hdpi/ic_settings_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_settings_white_48dp.png deleted file mode 100644 index eabb0a2ba41bbe0e109263ee9b0d6ada40a6e792..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 843 zcmV-R1GM~!P)RPWZAHfP!)RVB2S(!`lt|Um(uYf2_cGfFjG~?QJ_SKM>Hu7Qz_%` zC^KY0j^p?%F-hrI#t?oEkx>e=^x$WZC9-20$8bAJMiyi_f!j|s$&C!#uq!e{4&*3d z*U4D<;$4cQ>yQUJNq3uf6oz@6$E?Ckd0-Y5%+4`a;aG`TuRL)UvsTrDjhJ=G6Wy3? zP%Zcjv%EY}z-*IhK|5x7d7^;XCe?zCn03h$-I#4qEm(P<#_c#+Sul>1xE*7f>{!k} z_~{|56pZ6*{0y;7>1g5}{!WmS0WF-w-(4mv4bw>oQKFMsszM7}DHGx$QauHt_cw57G zmNrf?VwFPCz(EGt%|eYrmJb-fVG|Z VF?y~eTbBR;002ovPDHLkV1i=jh>-vQ diff --git a/base/src/main/res/drawable-hdpi/ic_text_fields_white_48dp.png b/base/src/main/res/drawable-hdpi/ic_text_fields_white_48dp.png deleted file mode 100644 index 2d76a1da723ee35408f0accd022ba7002bc11ee7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawd_7$pLo)8Yy>gJ3L4kw8&@H`q zj$?AuqVgTI|NFiM1M8{?msv%Cf(z(j_iM;~` z)+Qm8XKdw5=^VHd9y|!)ASb3$fM&{3{1?JePIK4O>cKg@cUCK|gyxYiC^Ljk&Yj?F-{IHw@C`b7C!s rSKQPz@_tJSxOR*f`WzfzT3xOme0M;f#XWi%00000NkvXXu0mjf^hM0B diff --git a/base/src/main/res/drawable-mdpi/ic_action_reminder.png b/base/src/main/res/drawable-mdpi/ic_action_reminder.png deleted file mode 100644 index 95e7cf87acea88bc831f4bf7c489e1f4608c238b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 720 zcmV;>0x$iEP)2m9)iZC$=8B0a{BDn5uZE3u=iv>GvEo>p+4yI`L@iByMXyG+sr?x~Hl{{+^9Ch$nd z`je8c2h{6wOi-|A63mc88^CjK8AV_k9FaLJi6~F@ih^AC9q~H{c7sh7{1<8&I{y(F z3n{}>6`(efgx@Lo7Fz^#k?Ot}yF>wM zcb4LHS_BlSrgJqA&_V(RECSp{G?rjz5}>y4)%LYTz%REK)KEZ4ZGTtpSwtLe60000!9aWp z$s!PY84&ja@oXSIjOLI&AZDee&yg*!Mh|KrpB;$9fcOBCx+-c|4l;NvlA!?v)GGk- zE+q9vlsX`gR38D=St99~M5zNNBgMHW3Hs(i^_>J_Zb}@m8*0#E67?k^g@OSk4uB=T z9uoC=BRRmH5(mJt*9;Q%g&{e>f)WQTff{m7oK(jx}MN1=LF zQ<@8;fcON|uzf%*OF%s{5KlvLbQm=XBjiv3RX`?qe2#4SGO85EKtotS1qxbOKMRQC zfY<|wLx8v$h>xJDsQ_X#YF5Mb1ZqMUUJk@HNc<#f){17(U_OP#;&nhA4hnW?1$Kre zj(|FZ4~UI{*a?WOfmj-gnsk~vf=GvsI%3c|0@RQd87N19EY}{$ro9-$C>RAe0036j V^0DEMkhTB-002ovPDHLkV1l;_qw@d& diff --git a/base/src/main/res/drawable-mdpi/ic_add_folder.png b/base/src/main/res/drawable-mdpi/ic_add_folder.png deleted file mode 100644 index 7cd7a48c597bf1351eea8a737c09120c6908b32f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJi=HlyAr-fh6C_v{Cy4O4efa;s zo>Ac(^P`gvHmuxvY)Pz9Rtq>g&I=s4-4M|l#&^T3g*l<`gnEOc;!DOOI!i1PtS9m* zu!>K7)F5k8a6l}|`T>K0KI@~)7Ej7A-eNR!IKy$onC%qfDwZMvPre(LA~FR$9bZ4p zILLJ5>z0xOiVD*XC9??&JilPYRmS+SNMl;_tFuj?!Wln$7RheNUT~SglFy*&)mv!| z_BD);*c9v>dD$;4(!1f;#T=FKpg}Cl%){g0q>8}CM Oa}1uYelF{r5}E)Tb71}e diff --git a/base/src/main/res/drawable-mdpi/ic_add_search.png b/base/src/main/res/drawable-mdpi/ic_add_search.png deleted file mode 100644 index 386397d36aa0e0eeb886097e8d8b688542af3571..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418 zcmV;T0bTxyP)(Oc{iiyin#xOD(0$JFCC#`+Kh7SWrD)0swXsKCG!LDT@8D&7 zSXDyP+SDJ~thDdY?#e)?5}L&Oji#ID1Mn(c-t|3@({!8U2QUP!I`g!4{-X6~=x}Yo z6xy^*nBNDu)8$mPikJ0{81Zo<&ghb<)Oio=L6?e~XC)Z(VZwByALd~RrtN{_|ESfK zl9x`zFgb3SFoQYV3=^gkMVTlz)5K3FPMh@wWlUVaLinBx5#nFRH{l64qw=CISO5S3 M07*qoM6N<$g5fZ}cmMzZ diff --git a/base/src/main/res/drawable-mdpi/ic_add_white_48dp.png b/base/src/main/res/drawable-mdpi/ic_add_white_48dp.png deleted file mode 100644 index 67bb598e52a36b6caba846efe733501479965a41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUiWQz^WKM^t3LcZ=|iRG zk3A|r|7Kn86;|8-bWh=-C*kYF)Lr&5&Uo;X@5(!l#?P`3Sk9L-2rJ!heDlxx;Ahzb zN75B4Swsr!?^|9DpJK!4H0Oo8of?0G0Q-OSVg>6149q+qemj&kF#hAJU@}d}o}j7} RtqpW5gQu&X%Q~loCIA@?R-pg@ diff --git a/base/src/main/res/drawable-mdpi/ic_border_color_white_24dp.png b/base/src/main/res/drawable-mdpi/ic_border_color_white_24dp.png deleted file mode 100644 index 2840abb89e201f093b4d3746dc07b614c682d5a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iG*1`D5R21K&nfaX7znsroEpOu zw1cVLsX$;t3|I5QcgfS(wg+EtS@~B#dUJwLjLx#Yi982ZHO(#mC@R$1cle;>^O-Rx zeC3}vOzw3&Jy&DCBb)uHvs%TQPyJ!uvWLMmKkH2CddCk8Ox9_0mkJd30c~XPboFyt I=akR{02%T+!Tv)bb-V-Eb zi4?l8xhNQ#apbh7+L^t_xaXLzD?HzyU%)R}?^0sX(!;G-carn;8^g?N$8BZbP0l+XkKWHLOn diff --git a/base/src/main/res/drawable-mdpi/ic_drag_handle_white_48dp.png b/base/src/main/res/drawable-mdpi/ic_drag_handle_white_48dp.png deleted file mode 100644 index aa1547b049d5891ce2d79ecf895db11a103cc186..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUBlKmvv4FO#n+h7OMaN diff --git a/base/src/main/res/drawable-mdpi/ic_fiber_smart_record_white_24dp.png b/base/src/main/res/drawable-mdpi/ic_fiber_smart_record_white_24dp.png deleted file mode 100644 index 7d5473cff111fa1c20f5259e8c817b1e3e7ab8e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 263 zcmV+i0r>ujP)_$q#txVjt5*&I!FK7$#YIYX5x$uNaS!lX#`AFDH&=Dl1i zEH3%aIi?63_`nHfjmQAKavCg>kN($s@0@1wR**^dP N002ovPDHLkV1m6#b!Gqn diff --git a/base/src/main/res/drawable-mdpi/ic_fiber_smart_record_white_48dp.png b/base/src/main/res/drawable-mdpi/ic_fiber_smart_record_white_48dp.png deleted file mode 100644 index ba3256d86ab8b4b55a22bd2fee08a3c0135dcee2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 517 zcmV+g0{Z=lP)W6dk8M9hGK`I9iEZ0{k=V&N)@S2rN42`Dt#vZ$-*C6PAQ^v|3?n zA1Y29B#C`u-~bAH-4nAy5@jZf!TO34lf=YjlDZ%Ua@JU!H*=9*lIkWe2G&`4(djX1JP;cess2!>X3}U78%DW0CDP~@8wROf`iBo_s?RT~ z*B~#}gIu3!x>xnn=ITphwPT@uP<@q-1$s!To17Tfpn4a1GhZaBb7CORN4<^>OrcBy z_HCK}xTAVM6U2`FB(Ylz90O&iI5C{p#Px!q#+H8%l;;c)Hzw0S9N(E}&794A2IU*$ z#2fQzh;jRQqVNgrD>K9&lX(^K=S&m}+7re|Fov;@GC@l0W~dlc?`124Bnl>Tfo{BX zbB;;k0JL+QF%k!P)^L^wG|*2!4Lsm1tI3H6mNHy?WipwclG(sPn?Kx>_L_6~ zSYS@fkwc~`eJ-DJCg|m;xNUhRyFmZ#e0$!562*u7b~QN{sqXD&J~W=zU~#-*?-41-ak-hLd~yC?j`rmTOtqW1_n=8KbLh*2~7Y*oVMIEb_)E|q>@ zwManE`?-n**VTw7f!Db?_soof{rA~-S@b=&IASv7a;lS+#3kP61P7dP^@1n{r~mb03) zc?j%JS-?17mEHc1^rNrOJ~Oe~&p-eC^UtCyO*)fuQe>4Y_g|6%VwoI2{_d>%srJ7&|&vk-TbVh<5S;l;%8hhyk$Kc9U$ho zN!;a@as7tMKN^!>mP|UmC!qU^(pC>$p{$8fOT;)oR_iUC=UG0_`?VR+LIzJ)KbLh* G2~7aOZ8w1c diff --git a/base/src/main/res/drawable-mdpi/ic_playlist_add_check_white_48dp.png b/base/src/main/res/drawable-mdpi/ic_playlist_add_check_white_48dp.png deleted file mode 100644 index a94c5d035a7b8fd290e821336729a300b8c14b75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DEKe85kP61PQ&<@p964C*f0@~H zHhkVEoKVoG?#Q92%)u*DwdCe=xJC{Ou%kl_%AYpq3t zfm2{SzX2Od?Q;dz+H?-b`{GU)<_j>X%U%7M&v^dd?5pSfIc9!Z!(taW>yZ}FRt8U3 KKbLh*2~7YA6fmm* diff --git a/base/src/main/res/drawable-mdpi/ic_refresh.png b/base/src/main/res/drawable-mdpi/ic_refresh.png deleted file mode 100644 index 32022dc966116203f3e507de8b4e9fbf2dcad4ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 421 zcmV;W0b2fvP)-e98k-F_{@M>PIm{OTMordK%57}?m#R+OUuQvSdNa5191xw%hAda z=>!}A!^eO)oL-LL0b&&(jsfD;XmSNKcLbU^GZ04t@d+eHgp+HD91v?0as&=J3nWJz zBP&e+HMIfpenO7GBNvGj3N55sE(pXYp_*rr8G_76Y2r9ZSrn+)3(56C6r`0HBy-$J z(O86}QI-M+C?T1XM{4}TG%{1*0CwaOlN61uNE%rvZ~z;*4nU3qISL%0fMiZSDH^>g zE=VBegd&;aL5fBpq-?kxh?&TAz&xlKCrGX-ff_oITpxl*T?!sKeIPzWazO;utOzYY zV8y914s~aUaD*(maTtqM#fG3q2a;qCc>d9oD)rsW{!wOiVi}UW{yyV zCd`ur8~|#ziP5rcClpy;7r2s24Mgk2IZK^^iBh`hXFsLp$Hi^-XdC|o$nN|w}JYolv9aiaA3)AHJ#jL^+Vnw!iASI;~WiKXmD0RMEHO!?-j&6;kHOKLX;?@ zWGE7DPJUcs54$WqYC|tM?Dm*a5EB$J%My_f5ptOQ;JnO8utv}pJu;w=5<;?v>&>8 zhF?Y&EaUf-F4>V}ouDGUG9XTwpfwUQW0DePIU@2QLIJZP6AEI68g@DQ)P^_(>}pKO zkE?_$vBVfLVvMs)nQ&JW#5%6Ll?i8P;6j~onehS_p38;=e>tq*49Si(?RM$JF6|Z- zhLe1y=@Yj&%{<$LDKkeuxA{!dCfy3hWwyzZQVXYeKoyfe+#{|QF7lc(Gdc+Jm}KP& zE12AuD=c7gL#~j*pF diff --git a/base/src/main/res/drawable-mdpi/ic_text_fields_white_48dp.png b/base/src/main/res/drawable-mdpi/ic_text_fields_white_48dp.png deleted file mode 100644 index 612d143869d6e916d10bb7bc7cdfef5c79dec2cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUfe86CCvq5QbAW383f~9Z3DW!eu2#JboFyt=akR{0P&O@ AdH?_b diff --git a/base/src/main/res/drawable-xhdpi/ic_action_enabled.png b/base/src/main/res/drawable-xhdpi/ic_action_enabled.png deleted file mode 100644 index f88c7828ae451c4e62fbdde9d70b3fc26a68deb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 830 zcmV-E1Ht@>P)IrPeL3nU4#nH~nAq8X4Nrrs1mz>Xf+^SX^Wdik@)1A4oGbYS z@Zgbr`~o>=TYko~Bj4#vn6f2501%)^5dS}^8S;L?Ps~me%@&fps?^O-{oWb z`o7C@6`k>XBPH^cK11oeN&w3lkdIhW3E*u;Hn4?d?z{=`-RdXYxd%RY$$Mv(@1ApJ?wrYF(vgmIq$3^CM^8^r zCEzSzSb{o7ki?6?dVsqOp>z+;QjJAf)-?l9b=fHuHwz$=R! z{O%T@6|fYL6J$;X+_T7efTU_Uznl&lNm&PA2y<910-R(H$}qsWzXIla4_Ekm1mL1& zZWmy2+!MFS$G|po*`krC6mTPk(-N-H^6z8lD+L_(nYTG~WTIGm!e^;&@E9{5@TUXE zy*LB-+(Y^fpE>QJaunKsk}y1g7BJhSGXdudc#bxDY-j+yu*|6sy9$~aA$I{2JvqUF zfJXlf$O9Gvz5omcWC{4WnwI!3TJAw)?lEIOe4a+xq4#&+M>zxf0Zto2E|~c~?tQ=! z9|Jqdp{fY_tI0vXm30Wv-g;&a`*hve2Wq10D~N+;`lJmTUOm1 z*Y5&31S={#|eslizrWv0IU)GA*_PY zngnbY2$0+=Yz>0bF?59#ba1#5}c>y~jt8*`yt=ZBOf%QgNl#awC)vlWYl>wg)u` z;BI7}My|BJE5Fugc;N2%8vJf{uqrH6kJoA#I8!ign&R6g-Y+y-wnc||AHmF@CCrn z#oH?8cRK~!W@{UBTe$kF$T%Fvwy0&ks}y<{uBkU`(G4)i^zY7_lX!dj36&tk5*G?F z)chkT+}aBSQxfcFh4+KE`2n`V zInaxDfX>U&oL$i3-04+%oTQ0mVW z-nW6BBXszk_37f|miZaV_;Q&KVNfD7h1zhp;Psl(B(<0R*Fe9?X3~+4bfhC4?#CN2 WbfXiYN_6)C0000FbngrEGdi4vuhM!G~MCZQ(3Y(r}rQsyS@2W5@8vLB{JOKI{YQ;RZ7 zm-)8;>3To)sN3oDoO92;=i~Fa&hym!m+kKP^4jO~IiK@9Hwe_!)YR0})YMY2QmM2U z+5=sNMxpo67ibQefnGwxP#3i2e=2-EbRGH(%_q<#bdtc;YvIk%0Q4ha;bBcf2kNc( zR_I-gg@<*Apw>h2`*>f^ThI$=wa>-xhknHrL2wFCA9NZz3Z3V3VzL4VZ=sbw7T*^0 zpQls5BW@Yv_Zy(A(Dw*m6J8yTNM;!{B_vBLq9_d@DsoUVax{rgFY=|`rE%52^`$kLFQoJJq@K#pXk`JJ#FbKDR!?+SovKjtjQd}xR{6 zxB^fxB=gK-yiLNkJLC#Lw;`F|wwf03drkn*;tIevA)!kaVlCokNI2FTmsjBi;T$NY zy$ay(lmPpJ>$gE^Sr*c|qx0UMHN>Ef^BUIxsQvv(fH08K_4l>l|AsFep^k{wRleF+ zWBlDl=#}7i#+&9Sp9K#hAUuV(Ch)fq>WrBtGH?p2)E9g1s^m0&|T;@7fgfF%>*sR&&9fhkRnWlpb!*-LQn__K_OHHA(v$c^u42|APZ)I wke%WU5VBW%8H8h*EnZDcO-)TrO|2UH1L;IKyG5`Po07*qoM6N<$f=CdBwEzGB diff --git a/base/src/main/res/drawable-xhdpi/ic_add_folder.png b/base/src/main/res/drawable-xhdpi/ic_add_folder.png deleted file mode 100644 index bc0edf688eeaacd0fd059f49c40c733ca1c57bfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 477 zcmV<30V4j1P)pq3V*81VI< z)^A_}VF-~;X5w7tJ8(#|WbPc;Y#26?AcPP?2qAS~)h0p^OB?nOd81rxgp6Yu# zr~qfMO|RJs4}1x5+oS+1#=5hi1(-M1onQb4U;r-y6f8gL4q<>%;bFlb*$EH;cW6sYJhjR3{!Q^l7e&kj8SBOa)e{JZ#ZtOJ5=SGBlsfuM=tp9=&P;0>y& zz$knauz5A(h^rSQW*T@w!u3&lNzbNcO#1kWj7u|mCOPjL5JCtcgb+dq`A2>MUWYxG Th2fCT00000NkvXXu0mjf(mKk5 diff --git a/base/src/main/res/drawable-xhdpi/ic_add_search.png b/base/src/main/res/drawable-xhdpi/ic_add_search.png deleted file mode 100644 index 0484d65ff25f55e7b1e3a4738a83d7e90983db9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 866 zcmV-o1D*VdP)@$fNakOPaXT++5(UguCeieY}0*V zz_Fjv7J#&~z+05R0okTH(Bs%2(iVU-&H^6>xC83BL-g9uY70P@v%ptiK|r>f!DGjM zN?QQ72o21}fNZY-KOFl9wFO|l&;X?>tphX*07r$!s3it*vAIBaIQkpfl!>z*LlY-F8vx4Y9 z8m zk7j=lKp&6FDbMe6mpAU?JUzbQAdu~Z_8`=8gEU&Pe)lxP6ExuY8?;xz4;=b)1(#WidrDZDqwJ%auqc+Pb?2@Zhsy!abn z5L~3OH%)!Lj(a{!&t-gq(62rn0&I!~g>iYICM2pM2y-S0!WDYGnt~wJke+Hvhm21U z`cz++0}Mh!=s*jFF{}0$r2h~EX(v!0u_sau@;M#o`rH+%7I9%5@C0F4-PC9bg3!o+ s%s2toM@%M@$z(E_OeT}bWGYF20n8TEg=ws%)Bpeg07*qoM6N<$g3Z&Axc~qF diff --git a/base/src/main/res/drawable-xhdpi/ic_add_white_48dp.png b/base/src/main/res/drawable-xhdpi/ic_add_white_48dp.png deleted file mode 100644 index d64c22e9edfdcf9babea9681c44e2dee53a6d2f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^2_Vb}Bp6OT_L>T$m`Z~Df*BafCZDwc^5i{T978G? xlN*`>5BzUTIj7P7-(I3-fns}2dtZ?oBSS_(`pg9u0+k>GJYD@<);T3K0RZru9Krwq diff --git a/base/src/main/res/drawable-xhdpi/ic_arrow_upward_white_48dp.png b/base/src/main/res/drawable-xhdpi/ic_arrow_upward_white_48dp.png deleted file mode 100644 index 8ac0552c7a37489e82306723c6c814da946ec028..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 354 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z^Liz;uuoF`1aaa-@^eSY=Pne zRV{WL7o_cTCEv(a#b^du?Ed-gTz5v_lm_$7*P^o5ZoL+D`+i;CWOe&LC+yug?c{}P z?9_hL_#F5-ui>*T*0_-XuLNBe)3hCiGM918VJ5=$Qg2Y~_#?SN z$nL!=kY?aes%K+pIx(NILE%$6g92D+JM*(YzZs1mhSsz``*WMY_+j?{z2YC0?LIRm zsKy+ff6-W;A?#4-A7yKhfK7LJ=W^xw%zTFy*UbIa@RNy!LqNfyfq@a3@jxG0R Ysd2aV@GU$l0}ML`Pgg&ebxsLQ0Maao1poj5 diff --git a/base/src/main/res/drawable-xhdpi/ic_border_color_white_24dp.png b/base/src/main/res/drawable-xhdpi/ic_border_color_white_24dp.png deleted file mode 100644 index de9e509399f627d29ce1c40b1dc42fa068aec359..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmV<503iQ~P)^MO3IDcO*rF-nS@#4s;-?I4! zpPd2{6FwrrKxIB{f(z}G3F8BRgN7d5#8bgns++hfc)&@FtTnuyxUk^6KA%1 zev;^%%WD#ge19PM~)y}6N-$x)!~;!&SB zM-M3ZJYkYH7Ir)scjU#Fswj2CNQ)ybiO+3kF#Y25{oiq0GJnsK4FdLnh-#bTKZKhUB-N4VE-**DjT@91@qy`HXq JF6*2UngHS8SwjE- diff --git a/base/src/main/res/drawable-xhdpi/ic_chevron_right.png b/base/src/main/res/drawable-xhdpi/ic_chevron_right.png deleted file mode 100644 index d752e0346043692d32deb5b0a94e0f1a206e3758..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=OFdm2Ln>~)y}6N-$x)!~;^8kF z1ev8Rg?ZncKH(uKEf~Ca-Z8_;u7*NSp4rY|+Gl?wWj7yC+XHr`Mdxon)m?Q={`0Xt z-YW%X{n&VgL(=lJwaLe)ZV}J#_9n0>OC02IGiVa*`B2x=E2y;1ZH3*T>E9pB+Ad;$ z@ki*>ABykQ;`UU0d~~#1+4lRx;CuXN?R-I(6P4z%mvIp(8J W_bn@(T$m`Z~Df*BafCZDwc@+3T6978G? plO_5RMEgMr~bSAF9tVLf?}PES`qmvv4FO#tVb7(W02 diff --git a/base/src/main/res/drawable-xhdpi/ic_fiber_smart_record_white_24dp.png b/base/src/main/res/drawable-xhdpi/ic_fiber_smart_record_white_24dp.png deleted file mode 100644 index ba3256d86ab8b4b55a22bd2fee08a3c0135dcee2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 517 zcmV+g0{Z=lP)W6dk8M9hGK`I9iEZ0{k=V&N)@S2rN42`Dt#vZ$-*C6PAQ^v|3?n zA1Y29B#C`u-~bAH-4nAy5@jZf!TO34lf=YjlDZ%Ua@JU!H*=9*lIkWe2G&`4(djX1JP;cess2!>X3}U78%DW0CDP~@8wROf`iBo_s?RT~ z*B~#}gIu3!x>xnn=ITphwPT@uP<@q-1$s!To17Tfpn4a1GhZaBb7CORN4<^>OrcBy z_HCK}xTAVM6U2`FB(Ylz90O&iI5C{p#Px!q#+H8%l;;c)Hzw0S9N(E}&794A2IU*$ z#2fQzh;jRQqVNgrD>K9&lX(^K=S&m}+7re|Fov;@GC@l0W~dlc?`124Bnl>Tfo{BX zbB;;k0JL+QF%k!P)^L^wG|*2!4Lsm1tI3H6mNHy?WipwclG(sPn?SnrNv+qP3m%CT+Rwr$%^*0yci_D)Ynoqx)yr~CYFSL*)CHPb%$9>fU8 zFbu;m48t%Ca_q=?JV$%Rvy3toDwJ8qc-r$U=dy!TEApJko6Nz_Y~J8_@)8gec$&i` zT{Lh%a|vTM_fana000xXnRb!}Hs?i3MA69eG>8xWvxbMMkt9*%Hr5lzT5hHw3Rtb+ zGIEj(_F)uB7|vee38w+jjqu?1CKf-BHfKK2xvUH<( z;sczn^p9-o$bzefYn!MOPaNtPs_PWL0r#WB zJ)*#K*xl}3b92#Q7J2bRTb5xpRornRx*Q`4+=|`N;)XZTTcpa z{hN3^I*kz}PWB(8?a}FHQQ}VjMfPk}M~QEJ|DSWKI?S!`4|6-9(>9{Ok=Si1PRvG^ zsiMT&f&QO2(B)-Ope<_yovG${bU8Xyl!l|k4SD9E!%WpFTiUS#t104z`_SR;kS9BD z6))7&NY*y0PSWfpdihzbp?B}=QQ6Au*Wg53&gM2(xt(shdM z`zcPBiXRG$AS;8(d%lY=aOy8Xuoq2aVH3NHE9&Th(>ivPV7QDdTqv$MmN~dNM}p%` zJ@J7$-r#TCJSkyNpgn2&$yxo>@F1&k_qBvYOS+SmuCx#vKB6)C`&E7;4L{LB9Psuf z1&M|NuamTwL*4N7506MZTtbPYG;*PMz|Cq-m-yI|VI*NNyNf5Bda<1(g910QhB#Jp z9eGi}Y6<5jf7k9aY$A${JVjlI8(&v&4{an3)Nv292xBI9Qyb#e<565oYe^G%j^S0N z<7X-_b2K>#h%r3Ifs!4z1Fue*sV0 VOp(-Xr-1+f002ovPDHLkV1mcs-9!KY diff --git a/base/src/main/res/drawable-xhdpi/ic_format_list_numbered_white_48dp.png b/base/src/main/res/drawable-xhdpi/ic_format_list_numbered_white_48dp.png deleted file mode 100644 index e4a79b2cec5c983f4fe30ad71e9eb4f3d1069884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 277 zcmV+w0qXvVP)Po6!ZOc z9RL78lQRMY3@zY13YKfB*ov;gBXkzmLyV7gcg)`wbPR8+`!XYnKuJ34>|MR8Lmi)8*9v7CH+J`pfy^5M}!8m@> zy4UPaRy`7Cd#3)&=V#$FXXj5(D5|Sy@^gFM zt~z6${oI7Qe-%yLKNoY_J?mGgm?=Lep}a1j>Fv+SoZp`JtNoZ^KjT4qU4GO0pL1F2 rKSy)gJ+Bw~b1q-V<~%#h@ezlmdtwA89DTBKHb}_R)z4*}Q$iB}7u$lp diff --git a/base/src/main/res/drawable-xhdpi/ic_keyboard_arrow_up_white_48dp.png b/base/src/main/res/drawable-xhdpi/ic_keyboard_arrow_up_white_48dp.png deleted file mode 100644 index 42615516b9bd18eee30acca54796791dcb18d3c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg9(%ethE&{od-Ww}vxA6hBDY7C z�n`H{DRl$`|%k3~A&t1Ymr*DB&)R3-~l4+aPPdH-JcJ@5a;-}By^CK}#PK9sd@ zdrR-nc|uB`_o}EotM<4tbN##thVhn)#rv|HQvM}d*{HZYmtXc+bH2cQ`}v{%9_Od~ z-$GZqiam(3;Lq9uP zb{{ofF+<*SN8}#Xsh^z>RTnN_eYAW<;yR;>#mP zB_2gJIy{WxDHIfl;54zZ?(Dj(@bmNJ>M(=pX9D&J05vf@IA0RFIB;_s@2aW4t^P!s z?~T5q@wxa<`?f89Li_gpTlKB*dwsOb9#+$?m6!M>7BklVVk|aEPh1DJ$>V{EJl=XH?V({#-awY-(QL|0?dORlc*XtSl(} z&d3n4k$a}x{MZxU-fiZ-$+m%8aD#C}>V?${Gwe!OwoRt{8X v))3p#FqiR-(={Oe2E;4(u$C!;+}~cs6m#}#(>L)Q#vrbztDnm{r-UW|mL!4d diff --git a/base/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_48dp.png b/base/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_48dp.png deleted file mode 100644 index 767d066de4a1a5067ead23b6ef0276f34d58a24a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgUU<4VhEy=Vy~>_-I6=TA@%25u ztnc$SY`MTzw@K&Yf!}@x=cjGq2Py=D1sM{IECL4Y3``6R4}{JuG%zr6Tv%so;jsJE zzY_`1rrTE6-Q4^fOgTS4&u%dPXV$Bji}AmH{QS)Luj}0T_v|-*KD}^lM*W(r=0AUC zbJp$KR?l%EuWie-^Xxx7fvGiIRE5Ac^oF^bLDVb%unh^MQc%Q~loCIB>Wg(m<2 diff --git a/base/src/main/res/drawable-xhdpi/ic_refresh.png b/base/src/main/res/drawable-xhdpi/ic_refresh.png deleted file mode 100644 index 01376be9432f30971584275c072222c6e53ee25f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 804 zcmV+<1Ka$GP)aEs@7j2>RqJ>@xIR<*zLqSNP zhzIe|N(lvvHU3ZW4NO6nd9&Hsof#)D1K(R-zVkl3onO0?NQ~RKS!;vAV1jh=AdhtN zpj*<(lLO#So)iF4@?-#D$&&yeOMc`51SQW^1ucMzpft7Q{rU&$f(}4SpcJL#MfwTa z13h*PzJV-~TcZSF5;O~X3EBX4_+S_W;yozoJOt$T z3`!Gp83e)kN*H1`*#TWkAh`WF3>jCJRKtz&fe{RIs#B65LWsBm=!ax>$eIMe7AIfh zfDkgSAgP3wspMxsf4OO72num!WK6g+vSI|2eV=h82#Rq#NEPhqPCf(rEinFMVXtwC z>>W}%jcW+`R~BZJ4WutiI!!+a4126HEC859R*VNaO-}`eM;6x=9YKG(I_Lhrz;MkX zfF^RNlGb7N6nW-K76Hs7eJbiOs~~21ivXsPJ}u}lTYhM#x@`S{9kl8&t0QJfivTi6 zpK3bH7O?{jivXsOKE2RkRzl34Sp@LhM=`Ax21X~M{R=_*`&7j!xAo=A*?1`YJA0bi0mE0%5(N| z1>Zoe@U8Tj!Z_iL)XfsUk1z2v#yY9wL#_in5_gJ7CyxN|NZlqQoxBf#NAeya>Et;8 ikNgeN#JG){wRQ(fg;Bbl|F--90000T$m`Z~Df*BafCZDwc@?<<+978G? ulNp)<5B!(svb!+BIsJdnuk+t)}?IPi4`?n-BcG{I5ur- z(S>O`4876<#~6}k=`>Up4hWM~CUltm-)|Ri1Ni>m=Xw7B>2vXZK2lazR#sM4OxVB` zGWm@sMM^ z-;l({UGfbz*r=0l*o=*rbn~#1)BM5?7AY+r;d^YHV6D>PUbfT6IjR&Lx6#YE6o*+) zuBKoO-MHxHK}|s}>p4WgxIShmD&AzWe%_@-6DZ+pqS#BZCQ!mV^fOt5qM?`(rW@k` zYsq0Fe-hOnJV_2~IlvgxjZ&g0_?+}E6PL@R*DQCeAjPd$B{wAK#0SSo$OW75#b()K zHUs$LSLVnT+wsWmw*M;;$z=L|Dth}oRO&PU|Q78@DG)EG-;f#n348sTZV zVF~9kvs0GXg_$1ilRM_pLS`uz%MxXz$ZS8iD;g5KO+dIKazGp50^T5@IM7A7EpotC z!W~mwxJ0;WIiQwsy^0H?ge#H*l7zdexR6PK98gHOKye|H0y&_NaDn2&2;t_-0ZGDL zRb044xEeWN72z%_E_4yDUJlqsxMPX~IW&?Y+)+88op1pSBoqyIaEQ!Ol*$riq{ys= zxpK!+dNI=^OYFwX1(wJSoA?`3V^qijl?0d?Wut77&!^Zq%PbjU4n5fUgnXH!m=5f< zk}m_~a~ON=BxQz5hH%r$Y-PY44&!E!3Yp*}?#{7XA*iGWcbzi9S)vHoMVW4ZGIkRX z#g8(<%S4r;jjhy@q>w_AtYRDOq=;&>OpwD#d~}?I%ut7qs$`Bm_@-GlSja#4WRw!w z;XQoPAY06$A7At_L-u$UUpyrjB^DU#RVFRX2UE@R5j!x|C?D|xrk<0JScR#T@)5nxBvhE diff --git a/base/src/main/res/drawable-xhdpi/ic_text_fields_white_48dp.png b/base/src/main/res/drawable-xhdpi/ic_text_fields_white_48dp.png deleted file mode 100644 index f4e597a8e2e59435707a72d15d9cb2e1d7793f84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111 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 diff --git a/base/src/main/res/drawable-xxhdpi/ic_add_search.png b/base/src/main/res/drawable-xxhdpi/ic_add_search.png deleted file mode 100644 index 88ece0fd5a63c613b67b7f664bfa4512f265e562..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1439 zcmV;Q1z`G#P)xta1Xh66U_ICf9s>{ZZ?}Usd#x;18>2UXW#B2W7kmZ2 z2fu-n;BRu0e>)032k(MzaILI({>C@MzXN^&BcKe%Q&Of=egeC|jk0Juqc?#&!Tu>2 zI}Lw;m&_4hrY!w`!_NR$fos%SHd*~#2bl5SW~dp@7;|tJXqM&wGyWCe z118h)09bw)Vq$O3d)Qe3J_2VFjAqUm28X%2u{?1Tm=CT7OTkL86ZCT?l@i|bPq5kY zPF@`_pG!-h@kc=)*bQz2bt&Gr2p*tg2NRqy!a8C`-i&_<*aA)^tXy6N?dOv;@joq0 zxL+j92Af%n^X7o13>7Y;{KZ7sQiIQ04E81D7+dzv&XWW7dI~EGI~!`XRJ5=K7P6=- zd1hNzUW|WNLQHx!Nc@rvH*ac^bO~{94Q8(lm%kC@|0n@<&cY~iD54}C`SZvqqQ^#0r(`QbM z4p_@jQ|+Nw1s=;>@P=b7wuv)0Mh85?3b)!rFAF>t+xU6GQ;W35=zz_hDy z)l-i|>40ue6}l+!Sn9bXt~QpjSRL@NqXsZmT!1ChaEkb4{Nalma-*s!V~}F-Jaz6pC23#Z$?=kU<~^_^+<1& zE1*dRc-~`x@4)gbLI6WxmuD$#hkNfW#4bGKb)FDVW+-Z_@n_Bg_pr}1?g)YVquz{} zWRPb)Rq+{aOB;bE@`OQs^fGA7lLP7) z68b&IQ%X$OPq2n)n8JHoxASH$H%AiAi5`uz|DQ56JnG2@mGrTBww%XHnweD1*fV+B zqR9Nx3HE^BQ@rn3=)H*P=7ibcnUu%U%Z$|r*ht&K6V6YA=fEpGygNFDlhS9CZx89r z-E83Q)K<@CO%}(~M;)PWkZtpZyg9)_%HupDn6(3}+Fj*2AolM8tvrUKI$q5CID?xq zCFYvmv-`aaIj+}Rv9>9fcL&(3-@;|;a1D%a@9_zjqfHf`)06OjLkvf`Tp`)*nHzWk z`%NxKOH(lV7*|g3@j~~)$(JygaFZ@@4A2R=y+u>U8L@=7UiL7C4^`Z(DKmkav5tTj z*;2MTV4kb5WZ!Jgg5eZ1VN)URswl9Ey#3NTi542QF>N!iqCuCm0z1uCx~{iorNo?p6Xv9Jf?aD`qnwbf6OzJ_ZFeq^#j*=WNyVDAN!n$t z3smdK II;Vst04bp$H~;_u diff --git a/base/src/main/res/drawable-xxhdpi/ic_arrow_upward_white_48dp.png b/base/src/main/res/drawable-xxhdpi/ic_arrow_upward_white_48dp.png deleted file mode 100644 index 5e61c3d18dacd80e410b098b0fa092c4be3bc862..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 506 zcmeAS@N?(olHy`uVBq!ia0vp^6F``S8Ays|{O<-*>H$6>u0Z-f5n!FImnBdsZ%L3} zFhc{w|BA}0>iU|xS{t6#{2v(@7)w1}978G?-(ETEnd2zp`tWPe1rD76!8s@PGS{b> zvpO5F7#pyhJs`ky@Whh%LWd*owzfOXw_SPj(d+qIReR&sU$@@$*ILr9?fn-uo|EUM zsVWFI0EvkWLPDGxih_+yLV_9!4;S>bG#d%=o$|2IP=<1tx;!~F)D#!YXlY0iLgz?i z%ssw%qQM2T{$l0C1%~GnRd_-afh;abPUg@P9ug~@+zvU28VRwjngCR_{lZa2AoD_= zKRcRKKxwLs>=Tc711I#H#ehVO6Nc6g!E-COS37xrDKi2nEf=)N~+ zox)G&egF1({b8rcU-IftoCQ(|;VH6WvzGAh-ZHyY?d7?76H6m9CdH|m*loVBb^lGd vRi!JOWnTrfTU}Ym2V}a7!v(&4caEP}bIR)2W1&=F%rJPm`njxgN@xNAb2i$s diff --git a/base/src/main/res/drawable-xxhdpi/ic_border_color_white_24dp.png b/base/src/main/res/drawable-xxhdpi/ic_border_color_white_24dp.png deleted file mode 100644 index 43bd9729cf73aed8625e7cc53713bc731a3af01b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawUVFMYhGg7(dviDMAqRn$hut48 zO4?o!X=L8jz%XHyG@nen5Op>U}E~r;ZzsTh*&d4%)3LJiMLp z;w)iznd+cJ8&wq_b*;?t*es(h)OX^jdSY{V+W@i?RQ%h{9!_%jQfe+3>93esrje+{#&p6-^7Lv3Dx> zSv=|h6NmrFeL2c`P;xorkzJ209)~o|==M&Z!zEC?_H|D%!^}4iYYUAOR@Ymev+&aV odOY#V6^viaSW-r_4f8bt(HWQ;|~*a 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 Tl^P`i{mgTe~DWM4f$6daA diff --git a/base/src/main/res/drawable-xxhdpi/ic_keyboard_arrow_up_white_48dp.png b/base/src/main/res/drawable-xxhdpi/ic_keyboard_arrow_up_white_48dp.png deleted file mode 100644 index 62467ced43d56deb7698ac28b25c85b8470458dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 371 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q1xWh(YZ)^zFj{%KIEGZ*dV4+Euf;*c^`hJi zX6@W7^DNzj>~CotUEKBLTI_3SicG^e^%{^mWwTQR!)Sp>q z7pK{rF0>TRJv-Ut;yfEp;kRce>s*{@(22d7$+>cR>|glMfto#XVuflb+!ESSESEl6#9Bj*yf^WC6oFtE&m%XR~?qVjMBZB zz9oHyN&Ig1)0=0lD2bf?FYG>6rgDdMAbOJBa1Kk6~-y+W)VZAMXNjJzf1=);T3K0RYtYm)ZaT diff --git a/base/src/main/res/drawable-xxhdpi/ic_more_vert_white_48dp.png b/base/src/main/res/drawable-xxhdpi/ic_more_vert_white_48dp.png deleted file mode 100644 index b37b96fe13515466eb9f390cca1b0b5bf036a829..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q1xWh(YZ)^zFwXaMaSW+od~?$=>yU#?>qkqE z*@6118gh%aNPMwV+9n=QG-m<(id_LR`*yulyO74_6RFh&K2Sro}E$p0nVLs;}$ThM(uS7_ll8Oak%lD{m&zRCf<9+8WoVVPCUaw z>3Xl1en5irI#EZ%fF~|}<#HkCWrK@)rt+uhf2j?kt=pHt(mybK$Gc9BuvrY15BinCJRmIdb{_PnW$mpF8`uZuxvhoy+E7 z{r4c3w?Kia%4foJv+kZUp8uk>!botQPm8+n!;@FP-LvU1;$L*8`GrJ<0IQr!BfH={ ygIpCkmzh9@&y-4#A7;95U3Yn{4I)VXF@I=3;kLdlW*;zE89ZJ6T-G@yGywq8+@bRT diff --git a/base/src/main/res/drawable-xxhdpi/ic_refresh.png b/base/src/main/res/drawable-xxhdpi/ic_refresh.png deleted file mode 100644 index ae805a5cb10293b11b3dcafe7af7fea417c9c06f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1357 zcmV-T1+w~yP)#H_veYQ3Wy6HwJt=a%A>$J zB4z>YwY~zP6hYu4puc8E!yXaxg!nBv)UzGogBAsx1@r)J03HKg0lrlr#3Z1F?Z{&) zLQd{6Il)c?zIvD!rdb3{xgaUVDB!0{5hmG=G^V5B_uBx^0l!=bk>QECj0hh09%8=O za4|9W^Ac5aIvW1A1Mtqp61ClyfCZ{)M-|o?mXqxcLWEf>tO!V62RzVCwv-)dsJBIm zfSHuzrUyR)SOfeKB2>eUG}RkM+R>`f2|OS-xQDFB?I-=VBaL--ASdReslh)gC#$?e z1Yhk)bGX>_^yFJJP*?koyGHT#ztst#auCbK z3l<6xM_hS{nLzZKf~c6BLlz1ULmcY#nLu<{L2}B{Bw#?Etd(oqfC)rjDoEb0v$V)7 zj%If*AhD<*dD&tCt}5C#Z^8;ll|MOQu>fZjZCjxxcnI)WK{9Bu0LK(<%aUG=hld{l zZ2qq=Pyqjfh^z4-N`g_905%_J1NQ@pwu#XnrX(1Wy`bzHEEWrJ7?ENLQW9*CK3I*4SmWxPF2d1>%tYxW;D|Z{DSRvest~ z5t1f*XAF@Y{g*=#%U6v86ydiWeGnN^(I(1KeROym@l_=<<4z?pXCs2MBE6)>iVRf`cC_s*vsD*vPBDDy0 zwOr(ANFZ}Zeqn$=Sy@QnjUZCly0I-k-dfuNID|+cPS$hrhp9cboFyt=akR{ E0MG;=MF0Q* diff --git a/base/src/main/res/drawable-xxhdpi/ic_settings_white_48dp.png b/base/src/main/res/drawable-xxhdpi/ic_settings_white_48dp.png deleted file mode 100644 index 55492f6caab651f464cff5e820555053f01f76ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1606 zcmV-M2D$l(P)#7(@wkM03*(PI*71lVLWzMWdM2t*`%d%{Vq{Q<&PQ@R4E&Q^Qm#bWF#_OofhHu`y4f<34OG zQ|MTWjr9s0uVCXO);i7_JY!yf;R7}Q&Y5KflI)CCrcF@KIm5m&0abZ1Is%%W)C%nZ+yu=K} zgNb~=dHp;|lg30nzhf9zhS@;9#>AId&nKLBlp4i>hq=&B?jWNQsO53;xRz%Pm#YLa z{D_@g=-2X(sSI-|eRR;Ei_peNd^y3Lx(E$)(8s0ZnJ(XWfzpa}(n__?VJh43Z7Y*? z4%M{MNs-bv%P($bY$sUF42f1w6Ni3&AaNa!agwp!B7aEHMR|(sBToA$Ql4&7@`bwz z!4mnw<(wb{r>K<=JV7Yd$vtKqZAH7_!h?vb`{CJOy%uvs6+#+5v6a)UV>#f2fRmnCi_j|)ecBX`L1I`)>y0ajq| zb+U4c6pv8EMi(hLKqdRJQRHD#@_}YfV{Wb-VLs;iSs;IyMGt27$|1Tiv!9vri-|mk ziN$h=dol4W6BGv?V3cSI)XE|17$KTb?pJ(xo@hGd7(0mO8O4dWh~^v=B{?;>5>9(|3|sz=PQP3t73tSNJzB3^QAnxQPNT z9A%E&U@-+;IZRd-$Z-T$^0dho8J@aubYMnzhxA6u=N_$4WF_|GQ zZHb^@)DuFaVWIGr7 zrQ*N@j&NQd9W-c6Ttx?ixbi7$xKd-Hk#+os^LoiB9!%i{{>3)#AfvKT#~NJt4Yev8 zmvJX=@;-myT7BM8j|*2RR8%m8tpSCM9&B|fWc(RhFDhg_j;%)(GL~TLE`^M5VC!au zj49Z~!Lwt(FN8x|m6@E@oi(+{)is S>i8!OWUQyFpUXO@geCyD-z3)n diff --git a/base/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png b/base/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png deleted file mode 100644 index 165c907d96ff40b1a6345b1fa7919717f4edd8f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^2SAt+NHA0_4_pPLm`Z~Df*BafCZDwc@^n32978G? z-(K3t%b>t;*x+CNt}`(XORZL4pR-a$^(|0EUEv;urWT!Hj|;y~N(Q~E&Fd?i7C z!3+%y|KAxIzppo`tF4iYWSVn>fr0U{r;B4q1>>9Zwq7ibB5oIF&E~d}{=hE&=p1YK zf^{xO0xqs->RaZ2q4SFP-UkPtn(*D@T5|ve{keVM<(~xZ|L367?;E8Q26e3HNdxc0k3x!UIp6=3G)`dN*0P?vh35elB$Lqc z@779Zm8L@)U5}qH?Ozlv4Sb6uf5i znm%A*cj088+8_+nedf4SK@;<$md#a-58pMYByN4pwxz^j(uPp`4^uwnGCdFnr3^Gs hvFH1AbtZ&h1>@~k#~c%sSGj}4JYD@<);T3K0RSJ_{Hg!| diff --git a/base/src/main/res/drawable-xxxhdpi/ic_border_color_white_24dp.png b/base/src/main/res/drawable-xxxhdpi/ic_border_color_white_24dp.png deleted file mode 100644 index dd541e33fc4c56907b2a0133b8318ecb5f6ca038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 346 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z^Lfy;uunK>+O}@yoVeFTrZw= zSY+e$qGkTiudaShoE=INI@%^M)TbRkDgAn1?gP2(>q|S%T>G}6fA5>D;;GAK{n`At zY_sdLKQ%4;|HQCZNB%Anu=-pq@FwH$Wi_5>*}`iK?}u>a9dO~;{HDeG+w>1tIP;uZ zwqN?*C-5%n`qFtDLUz9`ZRvfK{8f0{s{c~*j%N4WZ)iT}7k={jH|KAjpLbhKe!gvS zUiL@KX&Cqqt;Ky2ERIv1ykf1 zGMONq4Q2ln9Bk(s9+ODUxwSd;9WyX+ dV4&f{Po~0T!Lt5|M}L6CJYD@<);T3K0RVkEp6~zw diff --git a/base/src/main/res/drawable-xxxhdpi/ic_drag_handle_white_48dp.png b/base/src/main/res/drawable-xxxhdpi/ic_drag_handle_white_48dp.png deleted file mode 100644 index 84164aaf8e3f1697e8190217fdeb1a7c9ac34a47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^2SAt+NHA0_4_pPLm`Z~Df*BafCZDwc^3*(C977@w zzrDN=sD|V4hQI$!e%LlBY`p5r@L;Frq1*%VP4)~6bh1AE6Ij8+_ylB*r>mdKI;Vst E06=RZTmS$7 diff --git a/base/src/main/res/drawable-xxxhdpi/ic_fiber_smart_record_white_24dp.png b/base/src/main/res/drawable-xxxhdpi/ic_fiber_smart_record_white_24dp.png deleted file mode 100644 index 9f2861fbd75c0cc980402fc262bad94ebe2d4dd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 999 zcmVSnrNv+qP3m%CT+Rwr$%^*0yci_D)Ynoqx)yr~CYFSL*)CHPb%$9>fU8 zFbu;m48t%Ca_q=?JV$%Rvy3toDwJ8qc-r$U=dy!TEApJko6Nz_Y~J8_@)8gec$&i` zT{Lh%a|vTM_fana000xXnRb!}Hs?i3MA69eG>8xWvxbMMkt9*%Hr5lzT5hHw3Rtb+ zGIEj(_F)uB7|vee38w+jjqu?1CKf-BHfKK2xvUH<( z;sczn^p9-o$bzefYn!MOPaNtPs_PWL0r#WB zJ)*#K*xl}3b92#Q7J2bRTb5xpRornRx*Q`4+=|`N;)XZTTcpa z{hN3^I*kz}PWB(8?a}FHQQ}VjMfPk}M~QEJ|DSWKI?S!`4|6-9(>9{Ok=Si1PRvG^ zsiMT&f&QO2(B)-Ope<_yovG${bU8Xyl!l|k4SD9E!%WpFTiUS#t104z`_SR;kS9BD z6))7&NY*y0PSWfpdihzbp?B}=QQ6Au*Wg53&gM2(xt(shdM z`zcPBiXRG$AS;8(d%lY=aOy8Xuoq2aVH3NHE9&Th(>ivPV7QDdTqv$MmN~dNM}p%` zJ@J7$-r#TCJSkyNpgn2&$yxo>@F1&k_qBvYOS+SmuCx#vKB6)C`&E7;4L{LB9Psuf z1&M|NuamTwL*4N7506MZTtbPYG;*PMz|Cq-m-yI|VI*NNyNf5Bda<1(g910QhB#Jp z9eGi}Y6<5jf7k9aY$A${JVjlI8(&v&4{an3)Nv292xBI9Qyb#e<565oYe^G%j^S0N z<7X-_b2K>#h%r3Ifs!4z1Fue*sV0 VOp(-Xr-1+f002ovPDHLkV1mcs-9!KY diff --git a/base/src/main/res/drawable-xxxhdpi/ic_fiber_smart_record_white_48dp.png b/base/src/main/res/drawable-xxxhdpi/ic_fiber_smart_record_white_48dp.png deleted file mode 100644 index c08b3ca4ee56702219f7c48c1b675bb135b4c02d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1968 zcmV;h2T%BkP)G0000MfNklvOJFCzj|@TR%>kAXmf4bw#{sFZF|_Z z-ANl`&$P~V+&LS4_jvv4*1c7ERqxj47~$m~uPSFNM}f1M$orYX-K=02dugSMKKkgQ zmA&j_1$Q%r_c4L9C@3B|2J;GLu#OI*XlEVMc{zh*Cr2$~nZYBtdYI{qp;ifD4WHy< zl@qF&#P4V$PObcg2~;a~0Kj8R;|66P=h46clCqy~ajs&;zuLk3IZoL`z)0pZ5auzQ zAdvw8=;C)=rEH1})m}+6D;zd1sC?`9pRYcsveoU>QLUH2zl$&W*U44z1`f|M0u5vWos_eQ5 zGkx5ucyJ_7P>vp@PLWZ={g~N9mEyn@%GI|lqoKcG=96;A#q?6HdN^MesAdr+IvFB2 z1T3VS&9+X-G-BdDxnVrzZj`KW3eREU4%wr?7Rucw0$JfEx(T;j_IL?Cm?TTQFVQtX zz&3o)L?BDdOmGb_5?|cmP@XzOxZ7oe`S@bCtnr3~?n0c0PtK7wmJ_Cj(-ajA_~e_i z#?|x_=Cjdby#x4UA62r(ZwT`w1x3Lmd^1kg7)lRe#wZGYhi`r*2mF>WKbJXbX~#D$ z)W{l_5@tUInPMzH8Yu_NC(Mm9#SDBjMGklwVZI?#Jc5t*$N@DRqPSh=rDtEACI|e0 zFel0cufSI?l>_c1%=r8SC~m48P(@oTttza;S8L>i`zUUq!cm|DU$qj*0dJ?c7K884 z+4yXzWB;uC_h%wL8!HD?(&b$G;r;mR-EzWuihHBNFa@7A$O%89xGyOTcjL3a$O)gK zxZfxYEAZJ;IpNjL9TM!qXWQk3n<=hIVc3h$o|hBOp|}SXhE{ymEGL{!anC3WUHI&< zoNz3~9a0$j@L8XnP(^Xw`2eJT0J}FC~>f&2HTKs&x_A&>*!NpZ~t zd1F>FtL23IDQXn@T3YcrnT6L_Zp=i-yIWsOyY>9Gp& zn1?TB$r^7V+&!|vaC~vQEO7=cgu7KX2-t!THWA1Ya}w>e;Dz{Lye#oSOxz}01T<3a z))UAIx6@jH6hNnO<&Ee3%Y-0g_>C!_?;$ zCu-S2IoV2$BH|7XVrnH7iWisANx5j}LPbJ>kJE#xLkw2@c$KeDN90UaVy>STDj7`i z@BMjH@Lt-8;uA^=70e{-?xjM;xSef8^HU{>BWWVbHc&4^T*-YzwTMb3i&J=rtlG^< zGQthaCc4cWqvSD&C&;45IYXg1ns>7W8%-Rqfx%fB9D>IgCIfU}r;+0{a2U)(WXT@R zkdgHK`$KRFjbz0JPLd&P%;#tgJdR{0S#U4)GDdXYr$VJc1(Qj;Z$HVCL_9m_TX5a2_Kq9_9{N z!bBriD1E5sTl7$B-F%HYStHEfcqav=73VXDQhI=M<%DAT`6(wT?HI)-N?|?2<*dj* z|6Z<8HV`n0CI-?b#uG>h4M0DCc0001k{H^y6q5=Q_00000000000GI$=H=B9Yf1?xt0000; diff --git a/base/src/main/res/drawable-xxxhdpi/ic_format_list_numbered_white_48dp.png b/base/src/main/res/drawable-xxxhdpi/ic_format_list_numbered_white_48dp.png deleted file mode 100644 index 5eb047e8ed55a54f59887c2a8d5f8efd23c139ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 490 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE1xWt5x}=AJfpMd!i(^Q|t+#h}`!YKUG$eXY zP||vEk(n{o!9kT}nXpU%hfoM3lh=blhI0uWbI!~k&e8q@tb{Bdt?@&Jv3 zfg2SY+c-HD7??O37?>0e{O1m`dJ(S#{YoE|pefU9ES;!2WVmU)9CcqCh=Z!TIT>W{2-?iM;gZzU7nL6|>7M)ArB# zIk{(w!fu(#DU6(cpZTXeF!}$7bJMx-I%$Q2^M5|~D=_=dzyM~@^|z4u>Pm|&&Ea}oq~T8g>*{(1q$g@{1X(?srgsN^f_F3#{O9c{#<3bIcxt6 z5dCbunnAs3zr%g;YTPd^%wxHZ+iCM$KceT%gpz8Dq{^ekYJX)?Bg^g SVZIJi5YN-q&t;ucLK6W0h`tE` diff --git a/base/src/main/res/drawable-xxxhdpi/ic_more_vert_white_48dp.png b/base/src/main/res/drawable-xxxhdpi/ic_more_vert_white_48dp.png deleted file mode 100644 index df34cfd4f67f9109acac2853067b5c3d1db0b40a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 641 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1lLLH0T!HleK?W{btHuGHWl$32 z7tHYT*U#C?Urx<9&dYIe-s?!tH(hhouJovWeWjwW^gAqqXXdxg=b_w+ZRgqf3@>r% z{nh*MiGhJJ*VDx@q~g}wTkd%*iUJN7oA@Rde|R#-#;^4M(ym!< zrYfb(y+xC()(1*5|JlCI%~H?s{0seUQl-hwUh3?OY)vL{a3)d@TBv3!TCFM zihF(10$G$ zAfDyDWBIYjFo-^U$Ae))~7K8V>!j<`VcL_un8o@!x}4)d#S-4#EAQF4>fC*P z0eLpNb73>;=Q14p+^b5|Ac>eP1A3lsc+(ZcIc10ufVzgQ_jBc4_d!@=KgsH{(NP5 z_Fk=GcJb$O;TiugJUjL$Tdl&Ze!_u2tt`*h&&vL+4`j{-s!=znw?EkZlRs(QnRuX) zz)-j@JY#>3-dPauvfqcSe@;xF#f4_<&(cFtaPIlpKaxoC^gx;0w~Tj>Vmlv5(9_k= JWt~$(697Y)FVdQ I&MBb@0L{N6bpQYW diff --git a/base/src/main/res/drawable-xxxhdpi/ic_settings_white_48dp.png b/base/src/main/res/drawable-xxxhdpi/ic_settings_white_48dp.png deleted file mode 100644 index 9e242e7748071f029d35d43492b9619b7c2104fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2248 zcmV;(2sihMP)G0000P&NklrVMn5aWc@&Elg(y z3`{nIA$HIXAwDd_4PnHE&7mF62P7zh>=$K|=Yfehm9a`KR0b!iN~&|MKWjFAAky9*ockpQ|Y#fFC^fbPn$;ROkxyKUI;mITnoz2IVMG`<~Ph-n-59^p%Az5o9qA7a}xuJuE29#ihy*=5%mRj>`rH%_ z;98~D%h6y;IYiIe7#)oOe+*R6UtZ_Ss9tvyYiS~t?NJCY1szq)XHX9> zJU^uopBgCX;l(JMVk*&bm+t_Xe1xt>)-b-?HJ^8gf_ErDeYP8Ukp^^C)6Z{!c?4C# zTn3_;!T*SgdS;;*z#Pg6YN5{nL-~|+)U%7I*-ahks3X^3fHFY5;48ow+5yqQ4SoXr z1pp9xd<2+==<);C0j{71(d7hLZUa1w=x(vg0D06Sx@+VrcLCNQF16lOfUnVsxKxsf zZUVfE=-J<01el5F-H&Vsi19JK6Q{_w8(;~ki6?Ca7(o-Mi3ElA0&Jvj;tg8?CXqzw zm~10J2Ky0us>n1SU=BjxPfQ2M;S56GrwlP0UWqe2{ zQSpq)0N2ohe{F1L27^&4HiVgMp$-4qxYk^N)%aG+k^pxf4C4vT;oDks0S@3(6N|}4 z1zZllq#2(Mn+xzh{=7+ki1U_P@n^re0FU5Df?`z2mC%A8zc3ddi}!Hv6ys0^Hi1uY zubctq0w`jvCUX3oka>&_OsXZ@dVn0x zV^Rl?`aGmJm9v<$1m$4MFy|~&e4i7}=L5`WBiBiQVYFjLIU}NQQa+2-n6Me;XxlO2 zdHP4^aW!*D5Y`N50cH_aocp8pz@`<1)4^b80fy2^ILlE{3$TuGDo_r0fN)-lUVz<% zQ+8Q>MauT*1*jsN;>+sMm>)(jz)1`DQ`*~9!ufaf0yGd#k<$R92tCjpjY&RM4V4DdLem~@UDl;sVg4wE|gnV$fI z*=+w@m4z+EyiE*p9bhaq*w9P?%Hpmefel9)<1)Z3ny_W7)c`xOz*j0kE zw1==M$y4?ZXbk6FY-?cx%EE4@72EcZXEVU9oW_MuD75hE|6g4A4-;(#D5VWoYRE$w z+;EQLN-M?Y0}NmTZq*q4Ur@wx++VdH)5pdJ63$u}23@oS=@lLM?`K2sS*9x-C%F^Z|o=UEPNk*HXP zG8rJ7(+GVh%|Ec|#|VA1ZCr{dN9fs$vJqe+Nkrc+GQmy&#Rf#*UPjppa5asjCK?!S zFM#54QWKBa{u-+jh~6Eizuf?e>4@Il<>B1CtkuD{g38qwt+WV#KY_zt4W9jNqX;-HV%jPem+G;M%rVU(W$ zil+hb8n&&a;gqQIcdUb)@4AIsO7DiV12T_Y*^LH?>4XEz?j8U=HO3^#ICe zfJ_detDfh%q1&emyNH6FT!s2<<9Lw+xT3HpNuZ zwh-$j*B`Z1s6a!hx0gmx1x1mDq7UI~_#Lc1Ka=gl6%K152C_4fC?+CV3 zq5LoU0k&YvCJCUkmDsXE0_bcWw#=0PI=d5FCQAUF6=2KN5{9-f(;K!0Ns5X8>UDA-4$WO2nnFOOjDo5JxCIKXXOae#%nFNpkvi}1j W#h1@&<)CH&0000S%uAU-YiVu%-n!R&W%p|2aW>@}woMfxCaZ#3(@=qq8>k-}mi^F3YtT-G@yGywp#JSm6( diff --git a/base/src/main/res/drawable/bullet_1.png b/base/src/main/res/drawable/bullet_1.png deleted file mode 100644 index 0d989f9e05e560f3eae8575d698d5086bd0c3a0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmV;^0xbQBP)yHRc147=e*wQbwB-3B$@>Tm31JaacYU+nkB_nsr}357zT zP>3wFVGvVU#}>};l%M?MDQDQsI;Jv^He?ZM<*6 zN{j@FXa@2MCtlKzXbA>gxQiR^&{6zGT`uCLb2JdYP=UR8VGotWd-Nv-ucRv5Q@3uuJTrAzD-sJ2-_Fhr~LXp-D5binC~P zO01z9+O!iZxPvyg#5}5@Q8_V-sc1A+%;FLnT@rK1M;aQXkzdSU6k3fEGdP4+hs=$S z5wwb!7&EDdX7zNxgN=0)BaB6}vC2kx55{~F(JayF7^D9|$|E$pB}Vv$X5TWvA9#dj zkDT7b`e$P{q1h%e!df(2>vWvdKUOjb%?61P+M!u9F+zUQ&?-V^=Q|1gOJ1g?_o2QV zI;D|M%;6ClUGgAU)w)`&r+m3m=0qi!mZf(9v6 z6njWSgY{w;xp_^{-Z<*dQJvoe=?~S!8F~|>9%1E7gedl@F8vuJ3lCM-E{#!^Z}{mO zWyD=%W*2_hOJ;An>r3&&3Zld-^dW+m{?J3bMSb4ml@C-A?~$FAL~vV#Rb&^xP>h|p zCyk916Ti`btGMARH6<8C(SsK_@q%7NNsx%45zBdmRgYOgV`4-LN->4=q+%q6Q%s~Dwf*#vu|FcRmb?*J<^yTS(p8H$3d8+Cy ziBBez$z(Dd%uPJYi~PuNrZJz*tY-lghVvsY@+>!Turwk{+{N3>pdQnChdU@squ_X6 zWeq{C;&qM{x9Gqf3?Ygk+(`#0^%^2uIwmjq74tSgg7nYA=*efIFjKcVK_%h z^0;#U$&qItPT(q@;SC0{0h`{5eU!-Y6Wh{4cP^xp z{aH$DMzWK{i-Y(Re^nkKFCkFi5vJqsPY#m! zuqQL|Q{!dw5&=bCqK2QD>=oaKrv*RLIbUMH`Bd>!Vdr?hx?}M34W$^5f?wcgEFC2( zUczfL4@%5~&gvAGVT_*|R zcDz22a5xjMUXn0+;dPb-M|-AW`js{*9MyltG%ZBY!y}j~lv1j%E0`V${`yZxt8s*+ zgrlgTRcRmeSK0KMq=?rs-7i5f4Q&}aq+D??L#qV2`gxeTO3LVp>Adj2|EnlT%GiNb zA^#63vL3C!I6!~2brhu@+>GfUaezZG-5l=xIYt~|4BAUl4<@3$Adc`n+QjnRS=Gx$;BakQ_+A-+O;Ol-KUp^nqb z&@L4l%9xH5M>q~sN$ePp_I$&Xl;foyyofeZ9AOmNi_O(mIVfyfrO3L5Y;3KkDD~le zv{mfjzO~4EQZLTM)ZKYC4Ch8^uPhBlY> z5)6-HnwZ*l1=^#6L@+crNGlmarDMUzrRPh+oEW0x;Rj$OW(Q0!9U0RF_^bRJD4_PKIWoN^paiW$KAjJDvU^C9XFH7WHOn5 Y0N*7SUiC~4X8-^I07*qoM6N<$g7c~kd;kCd diff --git a/base/src/main/res/drawable/bullet_3.png b/base/src/main/res/drawable/bullet_3.png deleted file mode 100644 index f9331e902f91096c822a4942e7c63da2874a8b87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeE3?v1%WpM*3=>VS)S0Mc#2YBskcL*rWSrX(I z%%ISJ{{Q|77dGgeov{umnD6Q07*fIb_UcB?1_uF`iw6%dx%6yp&vuZpJsq%t<@fQJ z@=d~eGp5&1>t#zzD}5~#KaKN(6q{Q{XQOD)=?jcY(=*zN47WIX^5_O=SQI=s(8$dG zpUvEP=N*k#H+2o){OXjATFuXK__>>1psDd@cCKYQPveUu`GHn5c)I$ztaD0e0s!UY BUJn2O diff --git a/base/src/main/res/drawable/get_started_background.xml b/base/src/main/res/drawable/get_started_background.xml deleted file mode 100644 index 0e0311f4..00000000 --- a/base/src/main/res/drawable/get_started_background.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/base/src/main/res/drawable/ic_scarlet_logo.png b/base/src/main/res/drawable/ic_scarlet_logo.png deleted file mode 100644 index c2010875dac917971578bc013d810353aa6406d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20225 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 diff --git a/base/src/main/res/drawable/image_placeholder.jpg b/base/src/main/res/drawable/image_placeholder.jpg deleted file mode 100644 index 5dae74b22894363ce28454f6f975aab7450bc555..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21330 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 - - - - - - - - - diff --git a/base/src/main/res/drawable/quote_end.png b/base/src/main/res/drawable/quote_end.png deleted file mode 100644 index dc8d74054d1a5ab2f85a791d2f6f88c66a6b9b85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgQaoK8Ln>~)y>O71K|sLupsw%B z+mfPh961z?zw^F#2dZFT2zWKQ>^`@EfLzc$7DE3tet@oYx55MzYytY1uwe Date: Tue, 30 Jul 2019 01:09:24 +0100 Subject: [PATCH 054/134] Fixing IME button for PIN and Fingerprints --- .../security/activity/AppLockActivitySpecs.kt | 32 +++++++++--- .../security/sheets/PincodeBottomSheet.kt | 52 ++++++++++++++++--- .../drawable/light_secondary_rounded_bg.xml | 10 ++++ .../res/drawable/secondary_rounded_bg.xml | 13 ++--- base/src/main/res/values/colors.xml | 1 + 5 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 base/src/main/res/drawable/light_secondary_rounded_bg.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt index 92d4c9cc..6273c03a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -3,19 +3,22 @@ package com.maubis.scarlet.base.security.activity import android.graphics.Color import android.text.InputType import android.text.Layout +import android.view.KeyEvent +import android.view.inputmethod.EditorInfo import com.facebook.litho.* import com.facebook.litho.annotations.* -import com.facebook.litho.widget.EditText -import com.facebook.litho.widget.Image -import com.facebook.litho.widget.Text -import com.facebook.litho.widget.TextChangedEvent +import com.facebook.litho.widget.* +import com.facebook.litho.widget.Spinner.onClick import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.security.sheets.PincodeSheetData +import com.maubis.scarlet.base.security.sheets.PincodeSheetViewSpec import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.getEditorActionListener @LayoutSpec object AppLockViewSpec { @@ -23,12 +26,14 @@ object AppLockViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop fingerprintEnabled: Boolean, - @Prop onTextChange: (String) -> Unit): Component { + @Prop onTextChange: (String) -> Unit, + @Prop onClick: () -> Unit): Component { return Column.create(context) .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) .child(AppLockContentView.create(context) .fingerprintEnabled(fingerprintEnabled) .onTextChange(onTextChange) + .onClick(onClick) .flexGrow(1f)) .child(Row.create(context) .alignItems(YogaAlign.CENTER) @@ -68,11 +73,17 @@ object AppLockContentViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext, - @Prop fingerprintEnabled: Boolean): Component { + @Prop fingerprintEnabled: Boolean, + @Prop onClick: () -> Unit): Component { val description = when { fingerprintEnabled -> R.string.app_lock_details else -> R.string.app_lock_details_no_fingerprint } + val editBackground = when { + ApplicationBase.instance.themeController().isNightTheme() -> R.drawable.light_secondary_rounded_bg + else -> R.drawable.secondary_rounded_bg + } + return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) @@ -88,10 +99,11 @@ object AppLockContentViewSpec { .typeface(CoreConfig.FONT_MONSERRAT)) .child(EmptySpec.create(context).flexGrow(1f)) .child(EditText.create(context) - .backgroundRes(R.drawable.secondary_rounded_bg) + .backgroundRes(editBackground) .textSizeRes(R.dimen.font_size_xlarge) .minWidthDip(128f) .maxLength(4) + .hint("****") .alignSelf(YogaAlign.CENTER) .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) @@ -99,7 +111,11 @@ object AppLockContentViewSpec { .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) - + .imeOptions(EditorInfo.IME_ACTION_DONE) + .editorActionListener(getEditorActionListener({ + onClick() + true + })) .textChangedEventHandler(AppLockContentView.onTextChanged(context))) .child(EmptySpec.create(context).flexGrow(1f)) .build() diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index 4098d205..d756ac26 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -3,14 +3,15 @@ package com.maubis.scarlet.base.security.sheets import android.app.Dialog import android.text.InputType import android.text.Layout +import android.view.KeyEvent +import android.view.inputmethod.EditorInfo import com.facebook.litho.* import com.facebook.litho.annotations.* -import com.facebook.litho.widget.EditText -import com.facebook.litho.widget.Image -import com.facebook.litho.widget.Text -import com.facebook.litho.widget.TextChangedEvent +import com.facebook.litho.widget.* import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge +import com.github.ajalt.reprint.core.AuthenticationFailureReason +import com.github.ajalt.reprint.core.AuthenticationListener import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R @@ -29,6 +30,7 @@ import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.getEditorActionListener data class PincodeSheetData( val title: Int, @@ -36,7 +38,7 @@ data class PincodeSheetData( val onSuccess: () -> Unit, val onFailure: () -> Unit = {}, val isFingerprintEnabled: Boolean = false, - val onActionClicked: (String) -> Unit = {password -> + val onActionClicked: (String) -> Unit = { password -> when { password != "" && password == sSecurityCode -> { PinLockController.notifyPinVerified() @@ -54,7 +56,14 @@ object PincodeSheetViewSpec { private var passcodeEntered = "" @OnCreateLayout - fun onCreate(context: ComponentContext, @Prop data: PincodeSheetData): Component { + fun onCreate(context: ComponentContext, + @Prop data: PincodeSheetData, + @Prop dismiss: () -> Unit): Component { + val editBackground = when { + ApplicationBase.instance.themeController().isNightTheme() -> R.drawable.light_secondary_rounded_bg + else -> R.drawable.secondary_rounded_bg + } + val component = Column.create(context) .widthPercent(100f) .paddingDip(YogaEdge.VERTICAL, 8f) @@ -68,11 +77,12 @@ object PincodeSheetViewSpec { .marginDip(YogaEdge.BOTTOM, 16f) .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(EditText.create(context) - .backgroundRes(R.drawable.secondary_rounded_bg) + .backgroundRes(editBackground) .textSizeRes(R.dimen.font_size_xlarge) .minWidthDip(128f) .maxLength(4) .alignSelf(YogaAlign.CENTER) + .hint("****") .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_OPEN_SANS) @@ -80,6 +90,12 @@ object PincodeSheetViewSpec { .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) .marginDip(YogaEdge.VERTICAL, 8f) + .imeOptions(EditorInfo.IME_ACTION_DONE) + .editorActionListener(getEditorActionListener({ + data.onActionClicked(passcodeEntered) + dismiss() + true + })) .textChangedEventHandler(PincodeSheetView.onTextChangeListener(context))) .child(Row.create(context) .alignItems(YogaAlign.CENTER) @@ -157,6 +173,28 @@ class PincodeBottomSheet : LithoBottomSheet() { .dismiss { dismiss() } .build() } + + override fun onResume() { + super.onResume() + if (data.isFingerprintEnabled) { + Reprint.authenticate(object : AuthenticationListener { + override fun onSuccess(moduleTag: Int) { + data.onSuccess() + dismiss() + } + + override fun onFailure(failureReason: AuthenticationFailureReason?, fatal: Boolean, errorMessage: CharSequence?, moduleTag: Int, errorCode: Int) { + } + }) + } + } + + override fun onPause() { + super.onPause() + if (data.isFingerprintEnabled) { + Reprint.cancelAuthentication() + } + } } fun openCreateSheet( diff --git a/base/src/main/res/drawable/light_secondary_rounded_bg.xml b/base/src/main/res/drawable/light_secondary_rounded_bg.xml new file mode 100644 index 00000000..1d0f36e7 --- /dev/null +++ b/base/src/main/res/drawable/light_secondary_rounded_bg.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/base/src/main/res/drawable/secondary_rounded_bg.xml b/base/src/main/res/drawable/secondary_rounded_bg.xml index 68db64ec..91b91dc6 100644 --- a/base/src/main/res/drawable/secondary_rounded_bg.xml +++ b/base/src/main/res/drawable/secondary_rounded_bg.xml @@ -1,15 +1,10 @@ - - - - + + + + diff --git a/base/src/main/res/values/colors.xml b/base/src/main/res/values/colors.xml index dba30112..64ca7ccc 100644 --- a/base/src/main/res/values/colors.xml +++ b/base/src/main/res/values/colors.xml @@ -31,6 +31,7 @@ #22FFFFFF #16000000 + #16FFFFFF #FFEBEE #e8ccd0 From ce3919af154d9c606e2f419ed4f81caa3861be74 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 31 Jul 2019 23:14:49 +0100 Subject: [PATCH 055/134] Making the selected notes sheet more usable --- .../selection/activity/SelectNotesActivity.kt | 5 +- .../sheet/SelectedNoteOptionsBottomSheet.kt | 284 --------------- .../sheet/SelectedNotesOptionsBottomSheet.kt | 335 ++++++++++++++++++ .../support/sheets/GridOptionBottomSheet.kt | 33 ++ .../base/support/specs/GridSectionViewSpec.kt | 138 ++++++++ base/src/main/res/values/dimens.xml | 3 + base/src/main/res/values/strings.xml | 5 + 7 files changed, 517 insertions(+), 286 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt index 9f4bc4cb..20228f5d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt @@ -9,7 +9,8 @@ import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.main.HomeNavigationState import com.maubis.scarlet.base.note.getFullText -import com.maubis.scarlet.base.note.selection.sheet.SelectedNoteOptionsBottomSheet +import com.maubis.scarlet.base.note.selection.sheet.SelectedNotesOptionsBottomSheet +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.utils.bind const val KEY_SELECT_EXTRA_MODE = "KEY_SELECT_EXTRA_MODE" @@ -54,7 +55,7 @@ class SelectNotesActivity : SelectableNotesActivityBase() { } } secondaryFab.setOnClickListener { - SelectedNoteOptionsBottomSheet.openSheet(this) + openSheet(this, SelectedNotesOptionsBottomSheet()) } recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt deleted file mode 100644 index d1658eb9..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNoteOptionsBottomSheet.kt +++ /dev/null @@ -1,284 +0,0 @@ -package com.maubis.scarlet.base.note.selection.sheet - -import android.app.Dialog -import android.view.View -import com.github.bijoysingh.starter.util.IntentUtils -import com.github.bijoysingh.starter.util.TextUtils -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.core.format.FormatBuilder -import com.maubis.scarlet.base.core.format.sectionPreservingSort -import com.maubis.scarlet.base.core.note.NoteState -import com.maubis.scarlet.base.core.note.getFormats -import com.maubis.scarlet.base.note.* -import com.maubis.scarlet.base.note.folder.sheet.SelectedFolderChooseOptionsBottomSheet -import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity -import com.maubis.scarlet.base.note.tag.sheet.SelectedTagChooserBottomSheet -import com.maubis.scarlet.base.security.sheets.openUnlockSheet -import com.maubis.scarlet.base.support.option.OptionsItem -import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase - -class SelectedNoteOptionsBottomSheet() : GridBottomSheetBase() { - - override fun setupViewWithDialog(dialog: Dialog) { - setOptions(dialog, getOptions()) - setOptionTitle(dialog, R.string.choose_action) - } - - private fun getOptions(): List { - val activity = context as SelectNotesActivity - val options = ArrayList() - - val allItemsInTrash = !activity.getAllSelectedNotes().any { it.state !== NoteState.TRASH.name } - options.add(OptionsItem( - title = R.string.restore_note, - subtitle = R.string.tap_for_action_not_trash, - icon = R.drawable.ic_restore, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.mark(activity, NoteState.DEFAULT) - } - }), - visible = allItemsInTrash - )) - - val allItemsInFavourite = !activity.getAllSelectedNotes().any { it.state !== NoteState.FAVOURITE.name } - options.add(OptionsItem( - title = R.string.not_favourite_note, - subtitle = R.string.tap_for_action_not_favourite, - icon = R.drawable.ic_favorite_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.mark(activity, NoteState.DEFAULT) - } - }), - visible = allItemsInFavourite - )) - options.add(OptionsItem( - title = R.string.favourite_note, - subtitle = R.string.tap_for_action_favourite, - icon = R.drawable.ic_favorite_border_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.mark(activity, NoteState.FAVOURITE) - } - }), - visible = !allItemsInFavourite - )) - - val allItemsInArchived = !activity.getAllSelectedNotes().any { it.state !== NoteState.ARCHIVED.name } - options.add(OptionsItem( - title = R.string.unarchive_note, - subtitle = R.string.tap_for_action_not_archive, - icon = R.drawable.ic_archive_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.mark(activity, NoteState.DEFAULT) - } - activity.finish() - }), - visible = allItemsInArchived - )) - options.add(OptionsItem( - title = R.string.archive_note, - subtitle = R.string.tap_for_action_archive, - icon = R.drawable.ic_archive_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.mark(activity, NoteState.ARCHIVED) - } - activity.finish() - }), - visible = !allItemsInArchived - )) - options.add(OptionsItem( - title = R.string.send_note, - subtitle = R.string.tap_for_action_share, - icon = R.drawable.ic_share_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runTextFunction { - IntentUtils.ShareBuilder(activity) - .setChooserText(getString(R.string.share_using)) - .setText(it) - .share() - } - }) - )) - options.add(OptionsItem( - title = R.string.copy_note, - subtitle = R.string.tap_for_action_copy, - icon = R.drawable.ic_content_copy_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runTextFunction { - TextUtils.copyToClipboard(activity, it) - } - activity.finish() - }) - )) - options.add(OptionsItem( - title = R.string.trash_note, - subtitle = R.string.tap_for_action_trash, - icon = R.drawable.ic_delete_white_48dp, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.mark(activity, NoteState.TRASH) - } - activity.finish() - }), - visible = !allItemsInTrash - )) - options.add(OptionsItem( - title = R.string.delete_note_permanently, - subtitle = R.string.tap_for_action_delete, - icon = R.drawable.ic_delete_permanently, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.delete(activity) - } - activity.finish() - }) - )) - options.add(OptionsItem( - title = R.string.change_tags, - subtitle = R.string.change_tags, - icon = R.drawable.ic_action_tags, - listener = lockAwareFunctionRunner(activity) { - com.maubis.scarlet.base.support.sheets.openSheet(activity, SelectedTagChooserBottomSheet().apply { - onActionListener = { tag, selectTag -> - activity.runNoteFunction { - when (selectTag) { - true -> it.addTag(tag) - false -> it.removeTag(tag) - } - it.save(activity) - } - } - }) - } - )) - options.add(OptionsItem( - title = R.string.folder_option_change_notebook, - subtitle = R.string.folder_option_change_notebook, - icon = R.drawable.ic_folder, - listener = lockAwareFunctionRunner(activity, { - SelectedFolderChooseOptionsBottomSheet.openSheet(activity, { folder, selectFolder -> - activity.runNoteFunction { - when (selectFolder) { - true -> it.folder = folder.uuid - false -> it.folder = "" - } - it.save(activity) - } - }) - }) - )) - - val allLocked = !activity.getAllSelectedNotes().any { !it.locked } - options.add(OptionsItem( - title = R.string.lock_note, - subtitle = R.string.lock_note, - icon = R.drawable.ic_action_lock, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.locked = true - it.save(activity) - } - activity.finish() - }), - visible = !allLocked - )) - options.add(OptionsItem( - title = R.string.unlock_note, - subtitle = R.string.unlock_note, - icon = R.drawable.ic_action_unlock, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.locked = false - it.save(activity) - } - activity.finish() - }), - visible = allLocked - )) - - options.add(OptionsItem( - title = R.string.merge_notes, - subtitle = R.string.merge_notes, - icon = R.drawable.ic_merge_note, - listener = lockAwareFunctionRunner(activity, { - val selectedNotes = activity.getOrderedSelectedNotes().toMutableList() - if (selectedNotes.isEmpty()) { - return@lockAwareFunctionRunner - } - - val note = selectedNotes.firstOrNull() - if (note === null) { - return@lockAwareFunctionRunner - } - - val formats = note.getFormats().toMutableList() - selectedNotes.removeAt(0) - for (noteToAdd in selectedNotes) { - formats.addAll(noteToAdd.getFormats()) - noteToAdd.delete(activity) - } - note.description = FormatBuilder().getDescription(sectionPreservingSort(formats)) - note.save(activity) - activity.finish() - }) - )) - - val allBackupDisabled = !activity.getAllSelectedNotes().any { !it.disableBackup } - options.add(OptionsItem( - title = R.string.backup_note_enable, - subtitle = R.string.backup_note_enable, - icon = R.drawable.ic_action_backup, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.disableBackup = false - it.save(activity) - } - activity.finish() - }), - visible = allBackupDisabled - )) - options.add(OptionsItem( - title = R.string.backup_note_disable, - subtitle = R.string.backup_note_disable, - icon = R.drawable.ic_action_backup_no, - listener = lockAwareFunctionRunner(activity, { - activity.runNoteFunction { - it.disableBackup = true - it.save(activity) - } - activity.finish() - }), - visible = !allBackupDisabled - )) - return options - } - - private fun lockAwareFunctionRunner( - activity: SelectNotesActivity, - listener: () -> Unit): View.OnClickListener = View.OnClickListener { - val hasLockedNote = activity.getAllSelectedNotes().any { it.locked } - if (!hasLockedNote) { - listener() - dismiss() - return@OnClickListener - } - openUnlockSheet( - activity = activity, - onUnlockSuccess = { - listener() - dismiss() - }, - onUnlockFailure = {}) - } - - companion object { - fun openSheet(activity: SelectNotesActivity) { - val sheet = SelectedNoteOptionsBottomSheet() - sheet.show(activity.supportFragmentManager, sheet.tag) - } - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt new file mode 100644 index 00000000..f7de74b6 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt @@ -0,0 +1,335 @@ +package com.maubis.scarlet.base.note.selection.sheet + +import android.app.Dialog +import android.support.v4.content.ContextCompat +import com.facebook.litho.ComponentContext +import com.github.bijoysingh.starter.util.IntentUtils +import com.github.bijoysingh.starter.util.TextUtils +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.core.format.sectionPreservingSort +import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.main.sheets.AlertBottomSheet +import com.maubis.scarlet.base.main.sheets.AlertSheetConfig +import com.maubis.scarlet.base.note.* +import com.maubis.scarlet.base.note.folder.sheet.SelectedFolderChooseOptionsBottomSheet +import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity +import com.maubis.scarlet.base.note.tag.sheet.SelectedTagChooserBottomSheet +import com.maubis.scarlet.base.security.sheets.openUnlockSheet +import com.maubis.scarlet.base.support.sheets.GridOptionBottomSheet +import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.specs.GridSectionItem +import com.maubis.scarlet.base.support.specs.GridSectionOptionItem + +class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { + override fun title(): Int = R.string.choose_action + + override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { + val options = ArrayList() + options.add(getQuickActions(componentContext, dialog)) + options.add(getSecondaryActions(componentContext, dialog)) + options.add(getTertiaryActions(componentContext, dialog)) + return options + } + + private fun getQuickActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { + val activity = componentContext.androidContext as SelectNotesActivity + + val title = R.string.note_option_font_size + val options = ArrayList() + + val allItemsInTrash = !activity.getAllSelectedNotes().any { it.state !== NoteState.TRASH.name } + options.add(GridSectionOptionItem( + label = R.string.restore_note, + icon = R.drawable.ic_restore, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.mark(activity, NoteState.DEFAULT) + } + activity.finish() + }, + visible = allItemsInTrash + )) + + val allItemsInFavourite = !activity.getAllSelectedNotes().any { it.state !== NoteState.FAVOURITE.name } + options.add(GridSectionOptionItem( + label = R.string.not_favourite_note, + icon = R.drawable.ic_favorite_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.mark(activity, NoteState.DEFAULT) + } + activity.finish() + }, + visible = allItemsInFavourite + )) + options.add(GridSectionOptionItem( + label = R.string.favourite_note, + icon = R.drawable.ic_favorite_border_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.mark(activity, NoteState.FAVOURITE) + } + activity.finish() + }, + visible = !allItemsInFavourite + )) + + val allItemsInArchived = !activity.getAllSelectedNotes().any { it.state !== NoteState.ARCHIVED.name } + options.add(GridSectionOptionItem( + label = R.string.unarchive_note, + icon = R.drawable.ic_archive_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.mark(activity, NoteState.DEFAULT) + } + activity.finish() + }, + visible = allItemsInArchived + )) + options.add(GridSectionOptionItem( + label = R.string.archive_note, + icon = R.drawable.ic_archive_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.mark(activity, NoteState.ARCHIVED) + } + activity.finish() + }, + visible = !allItemsInArchived + )) + options.add(GridSectionOptionItem( + label = R.string.send_note, + icon = R.drawable.ic_share_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runTextFunction { + IntentUtils.ShareBuilder(activity) + .setChooserText(getString(R.string.share_using)) + .setText(it) + .share() + } + } + )) + options.add(GridSectionOptionItem( + label = R.string.copy_note, + icon = R.drawable.ic_content_copy_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runTextFunction { + TextUtils.copyToClipboard(activity, it) + } + activity.finish() + } + )) + options.add(GridSectionOptionItem( + label = R.string.trash_note, + icon = R.drawable.ic_delete_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.mark(activity, NoteState.TRASH) + } + activity.finish() + }, + visible = !allItemsInTrash + )) + + return GridSectionItem( + options = options, + sectionColor = ContextCompat.getColor(activity, R.color.material_blue_800)) + } + + private fun getSecondaryActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { + val activity = componentContext.androidContext as SelectNotesActivity + val options = ArrayList() + + options.add(GridSectionOptionItem( + label = R.string.change_tags, + icon = R.drawable.ic_action_tags, + listener = lockAwareFunctionRunner(activity) { + openSheet(activity, SelectedTagChooserBottomSheet().apply { + onActionListener = { tag, selectTag -> + activity.runNoteFunction { + when (selectTag) { + true -> it.addTag(tag) + false -> it.removeTag(tag) + } + it.save(activity) + } + activity.finish() + } + }) + } + )) + + val allItemsPinned = !activity.getAllSelectedNotes().any { !it.pinned } + options.add(GridSectionOptionItem( + label = R.string.pin_note, + icon = R.drawable.ic_pin, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.pinned = true + it.save(activity) + } + activity.finish() + }, + visible = !allItemsPinned + )) + options.add(GridSectionOptionItem( + label = R.string.unpin_note, + icon = R.drawable.ic_pin, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.pinned = false + it.save(activity) + } + activity.finish() + }, + visible = allItemsPinned + )) + + val allLocked = !activity.getAllSelectedNotes().any { !it.locked } + options.add(GridSectionOptionItem( + label = R.string.lock_note, + icon = R.drawable.ic_action_lock, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.locked = true + it.save(activity) + } + activity.finish() + }, + visible = !allLocked + )) + options.add(GridSectionOptionItem( + label = R.string.unlock_note, + icon = R.drawable.ic_action_unlock, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.locked = false + it.save(activity) + } + activity.finish() + }, + visible = allLocked + )) + + return GridSectionItem( + options = options, + sectionColor = ContextCompat.getColor(activity, R.color.material_red_800)) + } + + private fun getTertiaryActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { + val activity = componentContext.androidContext as SelectNotesActivity + val options = ArrayList() + + + options.add(GridSectionOptionItem( + label = R.string.folder_option_change_notebook, + icon = R.drawable.ic_folder, + listener = lockAwareFunctionRunner(activity) { + SelectedFolderChooseOptionsBottomSheet.openSheet(activity) { folder, selectFolder -> + activity.runNoteFunction { + when (selectFolder) { + true -> it.folder = folder.uuid + false -> it.folder = "" + } + it.save(activity) + } + activity.finish() + } + } + )) + + options.add(GridSectionOptionItem( + label = R.string.merge_notes, + icon = R.drawable.ic_merge_note, + listener = lockAwareFunctionRunner(activity) { + val selectedNotes = activity.getOrderedSelectedNotes().toMutableList() + if (selectedNotes.isEmpty()) { + return@lockAwareFunctionRunner + } + + val note = selectedNotes.firstOrNull() + if (note === null) { + return@lockAwareFunctionRunner + } + + val formats = note.getFormats().toMutableList() + selectedNotes.removeAt(0) + for (noteToAdd in selectedNotes) { + formats.addAll(noteToAdd.getFormats()) + noteToAdd.delete(activity) + } + note.description = FormatBuilder().getDescription(sectionPreservingSort(formats)) + note.save(activity) + activity.finish() + } + )) + + options.add(GridSectionOptionItem( + label = R.string.delete_note_permanently, + icon = R.drawable.ic_delete_permanently, + listener = lockAwareFunctionRunner(activity) { + openSheet(activity, AlertBottomSheet().apply { + config = AlertSheetConfig( + description = R.string.delete_sheet_delete_selected_notes_permanently, + onPositiveClick = { + activity.runNoteFunction { + it.delete(activity) + } + activity.finish() + } + ) + }) + } + )) + + val allBackupDisabled = !activity.getAllSelectedNotes().any { !it.disableBackup } + options.add(GridSectionOptionItem( + label = R.string.backup_note_enable, + icon = R.drawable.ic_action_backup, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.disableBackup = false + it.save(activity) + } + activity.finish() + }, + visible = allBackupDisabled + )) + options.add(GridSectionOptionItem( + label = R.string.backup_note_disable, + icon = R.drawable.ic_action_backup_no, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.disableBackup = true + it.save(activity) + } + activity.finish() + }, + visible = !allBackupDisabled + )) + return GridSectionItem( + options = options, + sectionColor = ContextCompat.getColor(activity, R.color.material_teal_800)) + } + + private fun lockAwareFunctionRunner( + activity: SelectNotesActivity, + listener: () -> Unit): () -> Unit = { + val hasLockedNote = activity.getAllSelectedNotes().any { it.locked } + when { + hasLockedNote -> openUnlockSheet( + activity = activity, + onUnlockSuccess = { + listener() + dismiss() + }, + onUnlockFailure = {}) + else -> { + listener() + dismiss() + } + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt new file mode 100644 index 00000000..a290d2b8 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt @@ -0,0 +1,33 @@ +package com.maubis.scarlet.base.support.sheets + +import android.app.Dialog +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.support.specs.GridSectionItem +import com.maubis.scarlet.base.support.specs.GridSectionView + + +abstract class GridOptionBottomSheet : LithoBottomSheet() { + + abstract fun title(): Int + abstract fun getOptions(componentContext: ComponentContext, dialog: Dialog): List + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + val column = Column.create(componentContext) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child(getLithoBottomSheetTitle(componentContext).textRes(title())) + + getOptions(componentContext, dialog).forEach { + column.child( + GridSectionView.create(componentContext) + .marginDip(YogaEdge.HORIZONTAL, 12f) + .marginDip(YogaEdge.VERTICAL, 8f) + .section(it)) + } + + return column.build() + } +} diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt new file mode 100644 index 00000000..3e40f27d --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -0,0 +1,138 @@ +package com.maubis.scarlet.base.support.specs + +import android.graphics.Color +import android.text.Layout +import android.text.TextUtils +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.SolidColor +import com.facebook.litho.widget.Text +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.support.ui.ThemeColorType + +data class GridSectionItem( + val title: Int = 0, + val sectionColor: Int = 0, + val options: List) + +data class GridSectionOptionItem( + val icon: Int, + val label: Int, + val listener: () -> Unit, + val visible: Boolean = true) + +@LayoutSpec +object GridOptionSpec { + @OnCreateLayout + fun onCreate( + context: ComponentContext, + @Prop option: GridSectionOptionItem, + @Prop solidSectionColor: Boolean, + @Prop(resType = ResType.COLOR) labelColor: Int, + @Prop(resType = ResType.COLOR) iconColor: Int, + @Prop(resType = ResType.COLOR) sectionColor: Int): Component { + return Column.create(context) + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 4f) + .child( + RoundIcon.create(context) + .bgColor(sectionColor) + .iconColor(iconColor) + .iconRes(option.icon) + .iconSizeRes(R.dimen.primary_round_icon_size) + .iconPaddingRes(R.dimen.primary_round_icon_padding) + .iconMarginVerticalRes(R.dimen.toolbar_round_icon_margin_vertical) + .iconMarginHorizontalRes(R.dimen.toolbar_round_icon_margin_horizontal) + .isClickDisabled(true) + .bgAlpha(if (solidSectionColor) 255 else 15) + ) + .child(Text.create(context) + .textRes(option.label) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_MONSERRAT) + .textSizeRes(R.dimen.font_size_small) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 16f) + .minLines(2) + .maxLines(2) + .ellipsize(TextUtils.TruncateAt.END) + .textColor(labelColor)) + .clickHandler(GridOption.onClick(context)) + .build() + } + + @OnEvent(ClickEvent::class) + fun onClick(context: ComponentContext, @Prop option: GridSectionOptionItem) { + option.listener() + } +} + +@LayoutSpec +object GridSectionViewSpec { + @OnCreateLayout + fun onCreate( + context: ComponentContext, + @Prop section: GridSectionItem): Component { + val column = Column.create(context) + val primaryColor = instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + + if (section.title != 0) { + column.child( + Text.create(context) + .textRes(section.title) + .typeface(CoreConfig.FONT_MONSERRAT) + .textSizeRes(R.dimen.font_size_normal) + .maxLines(1) + .ellipsize(TextUtils.TruncateAt.END) + .textColor(primaryColor)) + } + + val visibleOptions = section.options.filter { it.visible } + val getComponentAtIndex: (Int) -> Component = { index -> + when { + index >= visibleOptions.size -> EmptySpec.create(context) + .flexGrow(1f) + .flexBasisDip(1f) + .build() + else -> GridOption.create(context) + .flexGrow(1f) + .flexBasisDip(1f) + .solidSectionColor(section.sectionColor != 0) + .labelColor(primaryColor) + .iconColor(if (section.sectionColor == 0) primaryColor else Color.WHITE) + .sectionColor(if (section.sectionColor == 0) primaryColor else section.sectionColor) + .option(visibleOptions[index]) + .build() + } + } + + var index = 0 + while (true) { + val row = Row.create(context) + .widthPercent(100f) + if (index >= visibleOptions.size) { + break + } + + for (delta in 0..2) { + row.child(getComponentAtIndex(index)) + index += 1 + } + column.child(row) + } + column.child(SolidColor.create(context) + .color(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .heightDip(1.5f) + .widthDip(196f) + .alignSelf(YogaAlign.CENTER) + .marginDip(YogaEdge.VERTICAL, 16f) + .alpha(0.1f)) + return column.build() + } +} \ No newline at end of file diff --git a/base/src/main/res/values/dimens.xml b/base/src/main/res/values/dimens.xml index a05cd116..a69e0dac 100644 --- a/base/src/main/res/values/dimens.xml +++ b/base/src/main/res/values/dimens.xml @@ -18,6 +18,9 @@ 6dp 36dp + 48dp + 12dp + 36dp 32dp 32dp diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 0d32ebb2..9ec99895 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -258,6 +258,7 @@ Are you sure? Would you like to permanently delete the notes in the trash folder? Would you like to permanently delete this note? + Would you like to permanently delete these notes? Delete Cancel Notes are deleted forever after 7 days @@ -441,4 +442,8 @@ Enter 4 digit PIN to unlock + + + + From 4147d750d81113c021a6ee9d071835bd2d1fe212 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 31 Jul 2019 23:34:43 +0100 Subject: [PATCH 056/134] Changing Format Bottom Sheet to Litho --- .../creation/sheet/FormatActionBottomSheet.kt | 78 ++++++++----------- .../formats/recycler/FormatImageViewHolder.kt | 6 +- .../recycler/FormatSeparatorViewHolder.kt | 6 +- .../formats/recycler/FormatTextViewHolder.kt | 6 +- .../sheet/SelectedNotesOptionsBottomSheet.kt | 2 - .../support/sheets/GridOptionBottomSheet.kt | 6 +- .../base/support/specs/GridSectionViewSpec.kt | 20 +++-- 7 files changed, 65 insertions(+), 59 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt index 80e68eae..6a3dd3be 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.note.creation.sheet import android.app.Dialog -import android.view.View +import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R @@ -10,33 +10,33 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.NoteImage.Companion.deleteIfExist import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity -import com.maubis.scarlet.base.support.option.OptionsItem -import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase -import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.sheets.GridOptionBottomSheet +import com.maubis.scarlet.base.support.specs.GridSectionItem +import com.maubis.scarlet.base.support.specs.GridSectionOptionItem import pl.aprilapps.easyphotopicker.EasyImage -class FormatActionBottomSheet : GridBottomSheetBase() { +class FormatActionBottomSheet : GridOptionBottomSheet() { var noteUUID: String = "default" var format: Format? = null - override fun setupViewWithDialog(dialog: Dialog) { - if (format === null) { - return - } + override fun title(): Int = R.string.format_action_title - setOptions(dialog, getOptions(noteUUID, format!!)) - setOptionTitle(dialog, R.string.format_action_title) - } + override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { + val activity = componentContext.androidContext as ViewAdvancedNoteActivity + + val sections = ArrayList() + val options = ArrayList() + + if (this.format === null) { + return sections + } - private fun getOptions(noteUUID: String, format: Format): List { - val activity = themedActivity() as ViewAdvancedNoteActivity - val options = ArrayList() - options.add(OptionsItem( - title = R.string.import_export_layout_exporting_share, - subtitle = R.string.import_export_layout_exporting_share, + val format: Format = this.format!! + options.add(GridSectionOptionItem( + label = R.string.import_export_layout_exporting_share, icon = R.drawable.ic_share_white_48dp, - listener = View.OnClickListener { + listener = { IntentUtils.ShareBuilder(activity) .setChooserText(activity.getString(R.string.share_using)) .setText(format.text) @@ -45,39 +45,35 @@ class FormatActionBottomSheet : GridBottomSheetBase() { }, visible = !arrayOf(FormatType.IMAGE, FormatType.SEPARATOR).contains(format.formatType) )) - options.add(OptionsItem( - title = R.string.format_action_copy, - subtitle = R.string.format_action_copy, + options.add(GridSectionOptionItem( + label = R.string.format_action_copy, icon = R.drawable.ic_content_copy_white_48dp, - listener = View.OnClickListener { + listener = { TextUtils.copyToClipboard(context, format.text) dismiss() }, visible = !arrayOf(FormatType.IMAGE, FormatType.SEPARATOR).contains(format.formatType) )) - options.add(OptionsItem( - title = R.string.format_action_camera, - subtitle = R.string.format_action_camera, + options.add(GridSectionOptionItem( + label = R.string.format_action_camera, icon = R.drawable.ic_image_camera, - listener = View.OnClickListener { + listener = { EasyImage.openCamera(activity, format.uid) }, visible = format.formatType === FormatType.IMAGE )) - options.add(OptionsItem( - title = R.string.format_action_gallery, - subtitle = R.string.format_action_gallery, + options.add(GridSectionOptionItem( + label = R.string.format_action_gallery, icon = R.drawable.ic_image_gallery, - listener = View.OnClickListener { + listener = { EasyImage.openGallery(activity, format.uid) }, visible = format.formatType === FormatType.IMAGE )) - options.add(OptionsItem( - title = R.string.delete_sheet_delete_trash_yes, - subtitle = R.string.delete_sheet_delete_trash_yes, + options.add(GridSectionOptionItem( + label = R.string.delete_sheet_delete_trash_yes, icon = R.drawable.ic_delete_white_48dp, - listener = View.OnClickListener { + listener = { activity.deleteFormat(format) if (format.formatType === FormatType.IMAGE && !format.text.isBlank()) { deleteIfExist(noteImagesFolder.getFile(noteUUID, format)) @@ -85,16 +81,8 @@ class FormatActionBottomSheet : GridBottomSheetBase() { dismiss() } )) - return options - } - companion object { - - fun openSheet(activity: ThemedActivity, noteUUID: String, format: Format) { - val sheet = FormatActionBottomSheet() - sheet.format = format - sheet.noteUUID = noteUUID - sheet.show(activity.supportFragmentManager, sheet.tag) - } + sections.add(GridSectionItem(options = options)) + return sections } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index 0c15020d..ddd101ba 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -14,6 +14,7 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.note.ImageLoadCallback import com.maubis.scarlet.base.main.sheets.openDeleteFormatDialog import com.maubis.scarlet.base.note.creation.sheet.FormatActionBottomSheet +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.visibility import com.maubis.scarlet.base.support.utils.maybeThrow import pl.aprilapps.easyphotopicker.EasyImage @@ -69,7 +70,10 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase } actionMove.setColorFilter(config.iconColor) actionMove.setOnClickListener { - FormatActionBottomSheet.openSheet(activity, config.noteUUID, data) + openSheet(activity, FormatActionBottomSheet().apply { + noteUUID = config.noteUUID + format = data + }) } imageToolbar.visibility = visibility(config.editable) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatSeparatorViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatSeparatorViewHolder.kt index 709c88ed..a0f6d847 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatSeparatorViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatSeparatorViewHolder.kt @@ -7,6 +7,7 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.note.creation.sheet.FormatActionBottomSheet import com.maubis.scarlet.base.note.creation.sheet.sEditorMoveHandles +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.visibility class FormatSeparatorViewHolder(context: Context, view: View) : FormatViewHolderBase(context, view) { @@ -21,7 +22,10 @@ class FormatSeparatorViewHolder(context: Context, view: View) : FormatViewHolder actionMove.setColorFilter(config.iconColor) actionMove.visibility = visibility(config.editable) actionMove.setOnClickListener { - FormatActionBottomSheet.openSheet(activity, config.noteUUID, data) + openSheet(activity, FormatActionBottomSheet().apply { + noteUUID = config.noteUUID + format = data + }) } if (config.editable && !sEditorMoveHandles) { actionMove.visibility = View.INVISIBLE diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt index 4a93dc58..48f6809c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt @@ -20,6 +20,7 @@ import com.maubis.scarlet.base.core.format.MarkdownType import com.maubis.scarlet.base.note.creation.sheet.FormatActionBottomSheet import com.maubis.scarlet.base.note.creation.sheet.sEditorLiveMarkdown import com.maubis.scarlet.base.note.creation.sheet.sEditorMoveHandles +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.visibility import com.maubis.scarlet.base.support.utils.maybeThrow @@ -76,7 +77,10 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder actionMove.setColorFilter(config.iconColor) actionMove.visibility = visibility(config.editable) actionMove.setOnClickListener { - FormatActionBottomSheet.openSheet(activity, config.noteUUID, data) + openSheet(activity, FormatActionBottomSheet().apply { + noteUUID = config.noteUUID + format = data + }) } if (config.editable && !sEditorMoveHandles) { actionMove.visibility = View.INVISIBLE diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt index f7de74b6..21b51592 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt @@ -35,8 +35,6 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { private fun getQuickActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { val activity = componentContext.androidContext as SelectNotesActivity - - val title = R.string.note_option_font_size val options = ArrayList() val allItemsInTrash = !activity.getAllSelectedNotes().any { it.state !== NoteState.TRASH.name } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt index a290d2b8..df98445e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt @@ -20,11 +20,15 @@ abstract class GridOptionBottomSheet : LithoBottomSheet() { .paddingDip(YogaEdge.VERTICAL, 8f) .child(getLithoBottomSheetTitle(componentContext).textRes(title())) - getOptions(componentContext, dialog).forEach { + val options = getOptions(componentContext, dialog) + var index = 0 + options.forEach { + index++ column.child( GridSectionView.create(componentContext) .marginDip(YogaEdge.HORIZONTAL, 12f) .marginDip(YogaEdge.VERTICAL, 8f) + .showSeparator(index != options.size) .section(it)) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt index 3e40f27d..1a549912 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -78,7 +78,8 @@ object GridSectionViewSpec { @OnCreateLayout fun onCreate( context: ComponentContext, - @Prop section: GridSectionItem): Component { + @Prop section: GridSectionItem, + @Prop showSeparator: Boolean): Component { val column = Column.create(context) val primaryColor = instance.themeController().get(ThemeColorType.SECONDARY_TEXT) @@ -126,13 +127,16 @@ object GridSectionViewSpec { } column.child(row) } - column.child(SolidColor.create(context) - .color(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) - .heightDip(1.5f) - .widthDip(196f) - .alignSelf(YogaAlign.CENTER) - .marginDip(YogaEdge.VERTICAL, 16f) - .alpha(0.1f)) + + if (showSeparator) { + column.child(SolidColor.create(context) + .color(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .heightDip(1.5f) + .widthDip(196f) + .alignSelf(YogaAlign.CENTER) + .marginDip(YogaEdge.VERTICAL, 16f) + .alpha(0.1f)) + } return column.build() } } \ No newline at end of file From d0ec3cd12ba21b6b6292b0ae656afd998da4dab5 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 1 Aug 2019 00:16:00 +0100 Subject: [PATCH 057/134] Improving Folder Selection Experience to Litho --- .../note/actions/NoteOptionsBottomSheet.kt | 10 +- .../sheet/FolderChooseOptionsBottomSheet.kt | 86 --------- .../folder/sheet/FolderChooserBottomSheet.kt | 27 +++ .../sheet/FolderChooserBottomSheetBase.kt | 165 ++++++++++++++++++ .../sheet/FolderOptionItemBottomSheetBase.kt | 74 -------- .../SelectedFolderChooseOptionBottomSheet.kt | 68 ++------ .../sheet/SelectedNotesOptionsBottomSheet.kt | 19 +- .../main/res/layout/bottom_sheet_options.xml | 29 --- .../res/layout/bottom_sheet_tag_options.xml | 49 ------ 9 files changed, 224 insertions(+), 303 deletions(-) delete mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheet.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt delete mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt delete mode 100644 base/src/main/res/layout/bottom_sheet_options.xml delete mode 100644 base/src/main/res/layout/bottom_sheet_tag_options.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index ccd15605..729f7b44 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -14,12 +14,11 @@ import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getNoteState import com.maubis.scarlet.base.database.room.note.Note - import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteNotePermanentlySheet import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.activity.INoteOptionSheetActivity -import com.maubis.scarlet.base.note.folder.sheet.FolderChooseOptionsBottomSheet +import com.maubis.scarlet.base.note.folder.sheet.FolderChooserBottomSheet import com.maubis.scarlet.base.note.reminders.sheet.ReminderBottomSheet import com.maubis.scarlet.base.note.selection.activity.KEY_SELECT_EXTRA_MODE import com.maubis.scarlet.base.note.selection.activity.KEY_SELECT_EXTRA_NOTE_ID @@ -299,8 +298,11 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.folder_option_add_to_notebook, icon = R.drawable.ic_folder, listener = View.OnClickListener { - FolderChooseOptionsBottomSheet.openSheet(activity, note, { - activity.notifyResetOrDismiss() + com.maubis.scarlet.base.support.sheets.openSheet(activity, FolderChooserBottomSheet().apply { + this.note = note + this.dismissListener = { + activity.notifyResetOrDismiss() + } }) dismiss() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt deleted file mode 100644 index e305890b..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooseOptionsBottomSheet.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.maubis.scarlet.base.note.folder.sheet - -import android.app.Dialog -import android.content.Context -import android.content.DialogInterface -import android.view.View -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.core.folder.FolderBuilder -import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.folder.FolderOptionsItem -import com.maubis.scarlet.base.note.save -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.ui.visibility - -class FolderChooseOptionsBottomSheet : FolderOptionItemBottomSheetBase() { - - var note: Note? = null - var dismissListener: () -> Unit = {} - - override fun setupViewWithDialog(dialog: Dialog) { - if (note === null) { - dismiss() - return - } - - val options = getOptions() - dialog.findViewById(R.id.tag_card_layout).visibility = visibility(options.isNotEmpty()) - setOptions(dialog, getOptions()) - } - - override fun onNewFolderClick() { - val activity = context as ThemedActivity - CreateOrEditFolderBottomSheet.openSheet(activity, FolderBuilder().emptyFolder(), { folder, _ -> - toggleFolder(activity, note, folder) - reset(dialog) - }) - } - - fun toggleFolder(context: Context, note: Note?, folder: Folder) { - val localNote = note - if (localNote === null) { - return - } - localNote.folder = if (localNote.folder === folder.uuid) "" else folder.uuid - localNote.save(context) - } - - override fun onDismiss(dialog: DialogInterface?) { - super.onDismiss(dialog) - dismissListener() - } - - private fun getOptions(): List { - val activity = themedContext() as ThemedActivity - val options = ArrayList() - val selectedFolder = note!!.folder - for (folder in foldersDb.getAll()) { - options.add(FolderOptionsItem( - folder = folder, - usages = notesDb.getNoteCountByFolder(folder.uuid), - listener = { - toggleFolder(activity, note, folder) - reset(dialog) - }, - editListener = { - CreateOrEditFolderBottomSheet.openSheet(activity, folder, {_,_ -> reset(dialog)}) - }, - selected = folder.uuid == selectedFolder - )) - } - return options - } - - companion object { - fun openSheet(activity: ThemedActivity, note: Note, dismissListener: () -> Unit) { - val sheet = FolderChooseOptionsBottomSheet() - - sheet.note = note - sheet.dismissListener = dismissListener - sheet.show(activity.supportFragmentManager, sheet.tag) - } - } -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheet.kt new file mode 100644 index 00000000..5bf40c06 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheet.kt @@ -0,0 +1,27 @@ +package com.maubis.scarlet.base.note.folder.sheet + +import com.facebook.litho.ComponentContext +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.note.save + +class FolderChooserBottomSheet : FolderChooserBottomSheetBase() { + + var note: Note? = null + + override fun preComponentRender(componentContext: ComponentContext) { + + } + + override fun onFolderSelected(folder: Folder) { + note!!.folder = when { + note!!.folder == folder.uuid -> "" + else -> folder.uuid + } + note!!.save(requireContext()) + } + + override fun isFolderSelected(folder: Folder): Boolean { + return note!!.folder == folder.uuid + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt new file mode 100644 index 00000000..603c7ca5 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt @@ -0,0 +1,165 @@ +package com.maubis.scarlet.base.note.folder.sheet + +import android.app.Dialog +import android.content.DialogInterface +import android.graphics.Typeface +import android.support.v7.app.AppCompatActivity +import com.facebook.litho.* +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.widget.Text +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.core.folder.FolderBuilder +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.OptionItemLayout +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.specs.RoundIcon +import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.ui.ThemedActivity + +data class FolderOptionsItem( + val folder: Folder, + val isSelected: Boolean = false, + val listener: () -> Unit = {}) + +@LayoutSpec +object FolderItemLayoutSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, @Prop option: FolderOptionsItem): Component { + val theme = ApplicationBase.instance.themeController() + val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) + val selectedColor = when (theme.isNightTheme()) { + true -> context.getColor(R.color.material_blue_400) + false -> context.getColor(R.color.material_blue_700) + } + + val icon: Int + val bgColor: Int + val bgAlpha: Int + val textColor: Int + val typeface: Typeface + when (option.isSelected) { + true -> { + icon = R.drawable.ic_folder + bgColor = selectedColor + bgAlpha = 200 + textColor = selectedColor + typeface = CoreConfig.FONT_MONSERRAT_MEDIUM + } + false -> { + icon = R.drawable.ic_folder + bgColor = titleColor + bgAlpha = 15 + textColor = titleColor + typeface = CoreConfig.FONT_MONSERRAT + } + } + + val row = Row.create(context) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + RoundIcon.create(context) + .iconRes(icon) + .bgColor(bgColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(bgAlpha) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.END, 16f)) + .child(Text.create(context) + .flexGrow(1f) + .text(option.folder.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(typeface) + .textStyle(Typeface.BOLD) + .textColor(textColor)) + row.clickHandler(OptionItemLayout.onItemClick(context)) + return row.build() + } + + @OnEvent(ClickEvent::class) + fun onItemClick(context: ComponentContext, @Prop option: FolderOptionsItem) { + option.listener() + } +} + + +abstract class FolderChooserBottomSheetBase : LithoBottomSheet() { + + var dismissListener: () -> Unit = {} + + protected abstract fun preComponentRender(componentContext: ComponentContext) + protected abstract fun onFolderSelected(folder: Folder) + protected abstract fun isFolderSelected(folder: Folder): Boolean + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + preComponentRender(componentContext) + val activity = context as ThemedActivity + val component = Column.create(componentContext) + .widthPercent(100f) + val foldersComponent = Column.create(componentContext) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child(getLithoBottomSheetTitle(componentContext) + .textRes(R.string.folder_option_change_notebook) + .marginDip(YogaEdge.BOTTOM, 12f)) + getFolderOptions().forEach { + foldersComponent.child(FolderItemLayout.create(componentContext).option(it)) + } + + val addTag = LithoOptionsItem( + title = R.string.folder_sheet_add_note, + subtitle = 0, + icon = R.drawable.icon_add_notebook, + listener = { + CreateOrEditFolderBottomSheet.openSheet(activity, FolderBuilder().emptyFolder()) { folder, _ -> + onFolderSelected(folder) + reset(activity, dialog) + } + }) + foldersComponent.child(OptionItemLayout.create(componentContext) + .option(addTag) + .backgroundRes(R.drawable.accent_rounded_bg) + .marginDip(YogaEdge.TOP, 16f) + .onClick { addTag.listener() }) + + component.child(foldersComponent) + return component.build() + } + + private fun getFolderOptions(): List { + val activity = context as AppCompatActivity + val options = ArrayList() + for (folder in CoreConfig.foldersDb.getAll()) { + options.add(FolderOptionsItem( + folder = folder, + listener = { + onFolderSelected(folder) + reset(activity, dialog) + }, + isSelected = isFolderSelected(folder) + )) + } + options.sortByDescending { if (it.isSelected) 1 else 0 } + return options + } + + override fun onDismiss(dialog: DialogInterface?) { + super.onDismiss(dialog) + dismissListener() + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt deleted file mode 100644 index 86b9d09a..00000000 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderOptionItemBottomSheetBase.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.maubis.scarlet.base.note.folder.sheet - -import android.app.Dialog -import android.view.View -import android.view.View.GONE -import android.widget.LinearLayout -import com.github.bijoysingh.uibasics.views.UIActionView -import com.github.bijoysingh.uibasics.views.UITextView -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.note.folder.FolderOptionsItem -import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment - -abstract class FolderOptionItemBottomSheetBase : ThemedBottomSheetFragment() { - - override fun setupView(dialog: Dialog?) { - super.setupView(dialog) - if (dialog == null) { - return - } - reset(dialog) - setAddFolderOption(dialog) - makeBackgroundTransparent(dialog, R.id.root_layout) - } - - abstract fun setupViewWithDialog(dialog: Dialog) - - abstract fun onNewFolderClick() - - override fun getBackgroundView(): Int { - return R.id.options_layout - } - - override fun getBackgroundCardViewIds(): Array = arrayOf(R.id.tag_card_layout) - - fun setAddFolderOption(dialog: Dialog) { - val newFolderButton = dialog.findViewById(R.id.new_tag_button); - newFolderButton.setText(R.string.folder_sheet_add_note) - newFolderButton.setOnClickListener { onNewFolderClick() } - newFolderButton.icon.alpha = 0.6f - } - - fun reset(dialog: Dialog) { - val layout = dialog.findViewById(R.id.options_container) - layout.removeAllViews() - setupViewWithDialog(dialog) - } - - fun setOptions(dialog: Dialog, options: List) { - val layout = dialog.findViewById(R.id.options_container) - for (option in options) { - val contentView = View.inflate(context, R.layout.layout_option_sheet_item, null) as UIActionView - contentView.setTitle(option.folder.title) - contentView.setOnClickListener { option.listener() } - contentView.subtitle.visibility = GONE - contentView.setImageResource(option.getIcon()) - - if (option.editable) { - contentView.setActionResource(option.getEditIcon()); - contentView.setActionTint(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)); - contentView.setActionClickListener { option.editListener() } - } - - contentView.setTitleColor(getOptionsTitleColor(option.selected)) - contentView.setSubtitleColor(getOptionsSubtitleColor(option.selected)) - contentView.setImageTint(getOptionsTitleColor(option.selected)) - - layout.addView(contentView) - } - } - - override fun getLayout(): Int = R.layout.bottom_sheet_tag_options -} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt index 4c69ec1f..8f7aa044 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/SelectedFolderChooseOptionBottomSheet.kt @@ -1,66 +1,28 @@ package com.maubis.scarlet.base.note.folder.sheet -import android.app.Dialog -import android.view.View -import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.core.folder.FolderBuilder +import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.note.folder.FolderOptionsItem import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.ui.visibility -class SelectedFolderChooseOptionsBottomSheet : FolderOptionItemBottomSheetBase() { +class SelectedFolderChooseOptionsBottomSheet : FolderChooserBottomSheetBase() { var onActionListener: (Folder, Boolean) -> Unit = { _, _ -> } - - override fun setupViewWithDialog(dialog: Dialog) { - val options = getOptions() - dialog.findViewById(R.id.tag_card_layout).visibility = visibility(options.isNotEmpty()) - setOptions(dialog, getOptions()) - } - - override fun onNewFolderClick() { - val activity = context as ThemedActivity - CreateOrEditFolderBottomSheet.openSheet(activity, FolderBuilder().emptyFolder(), { folder, _ -> - onActionListener(folder, true) - reset(dialog) - }) + var selectedFolders: MutableList = emptyList().toMutableList() + var selectedFolder: String = "" + + override fun preComponentRender(componentContext: ComponentContext) { + val activity = requireContext() as SelectNotesActivity + selectedFolders.clear() + selectedFolders.addAll(activity.getAllSelectedNotes().map { it.folder }.distinct()) + selectedFolder = selectedFolders.firstOrNull() ?: "" } - private fun getOptions(): List { - val activity = themedContext() as SelectNotesActivity - val options = ArrayList() - - val folders = activity.getAllSelectedNotes().map { it.folder }.distinct() - val selectedFolder = when (folders.size) { - 1 -> folders.first() - else -> "" - } - for (folder in foldersDb.getAll()) { - options.add(FolderOptionsItem( - folder = folder, - usages = notesDb.getNoteCountByFolder(folder.uuid), - listener = { - onActionListener(folder, folder.uuid != selectedFolder) - reset(dialog) - }, - editListener = { - CreateOrEditFolderBottomSheet.openSheet(activity, folder, {_,_ -> reset(dialog)}) - }, - selected = folder.uuid == selectedFolder - )) - } - return options + override fun onFolderSelected(folder: Folder) { + onActionListener(folder, true) + onActionListener(folder, folder.uuid != selectedFolder) } - companion object { - fun openSheet(activity: ThemedActivity, listener: (Folder, Boolean) -> Unit) { - val sheet = SelectedFolderChooseOptionsBottomSheet() - sheet.onActionListener = listener - sheet.show(activity.supportFragmentManager, sheet.tag) - } + override fun isFolderSelected(folder: Folder): Boolean { + return folder.uuid == selectedFolder } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt index 21b51592..31f9d1ae 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt @@ -225,16 +225,19 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { label = R.string.folder_option_change_notebook, icon = R.drawable.ic_folder, listener = lockAwareFunctionRunner(activity) { - SelectedFolderChooseOptionsBottomSheet.openSheet(activity) { folder, selectFolder -> - activity.runNoteFunction { - when (selectFolder) { - true -> it.folder = folder.uuid - false -> it.folder = "" + openSheet(activity, SelectedFolderChooseOptionsBottomSheet().apply { + this.dismissListener = {} + this.onActionListener = { folder, selectFolder -> + activity.runNoteFunction { + when (selectFolder) { + true -> it.folder = folder.uuid + false -> it.folder = "" + } + it.save(activity) } - it.save(activity) + activity.finish() } - activity.finish() - } + }) } )) diff --git a/base/src/main/res/layout/bottom_sheet_options.xml b/base/src/main/res/layout/bottom_sheet_options.xml deleted file mode 100644 index 96ce478f..00000000 --- a/base/src/main/res/layout/bottom_sheet_options.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/layout/bottom_sheet_tag_options.xml b/base/src/main/res/layout/bottom_sheet_tag_options.xml deleted file mode 100644 index bc7dac9e..00000000 --- a/base/src/main/res/layout/bottom_sheet_tag_options.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 0be778f39ac35df15eac837a0de76b4102fb3087 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 1 Aug 2019 22:54:05 +0100 Subject: [PATCH 058/134] [Selection] Improving Selection Experience --- app/build.gradle | 4 +- .../note/actions/NoteOptionsBottomSheet.kt | 20 +- .../folder/SelectorFolderRecyclerHolder.kt | 38 ++++ .../note/folder/SelectorFolderRecyclerItem.kt | 25 ++ .../base/note/recycler/NoteAppAdapter.kt | 6 + .../activity/SelectableNotesActivityBase.kt | 32 ++- .../res/layout/bottom_sheet_note_options.xml | 213 ++++++++++-------- .../main/res/layout/item_selector_folder.xml | 33 +++ build.gradle | 4 +- 9 files changed, 265 insertions(+), 110 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt create mode 100644 base/src/main/res/layout/item_selector_folder.xml diff --git a/app/build.gradle b/app/build.gradle index 7693e57f..f0d58fcd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 137 - versionName '7.0.7' + versionCode 139 + versionName '7.0.9' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 729f7b44..5d63a1c2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -4,7 +4,10 @@ import android.app.Dialog import android.content.Intent import android.support.v4.content.ContextCompat import android.view.View +import android.view.ViewGroup import android.widget.GridLayout +import android.widget.LinearLayout +import android.widget.LinearLayout.VERTICAL import android.widget.TextView import com.github.bijoysingh.starter.util.RandomHelper import com.maubis.markdown.Markdown @@ -79,16 +82,28 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { return } + val groupCardLayout = dialog.findViewById(R.id.group_card_layout) val tagCardLayout = dialog.findViewById(R.id.tag_card_layout) + val selectCardLayout = dialog.findViewById(R.id.select_notes_layout) + val tags = tagCardLayout.findViewById(R.id.tags_content) - val tagsTitle = tagCardLayout.findViewById(R.id.tags_title) + val tagSubtitle = tagCardLayout.findViewById(R.id.tags_subtitle) val tagContent = note.getTagString() if (tagContent.isNotBlank()) { GlobalScope.launch(Dispatchers.Main) { val text = GlobalScope.async(Dispatchers.IO) { Markdown.renderSegment(tagContent, true) } tags.visibility = View.VISIBLE - tagsTitle.visibility = View.GONE + tagSubtitle.visibility = View.GONE + tags.text = text.await() + + groupCardLayout.orientation = VERTICAL + + val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + val margin = activity.resources.getDimension(R.dimen.spacing_xxsmall).toInt() + params.setMargins(margin, margin, margin, margin) + tagCardLayout.layoutParams = params + selectCardLayout.layoutParams = params } } tagCardLayout.setOnClickListener { @@ -99,7 +114,6 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() } - val selectCardLayout = dialog.findViewById(R.id.select_notes_layout) selectCardLayout.setOnClickListener { val intent = Intent(context, SelectNotesActivity::class.java) intent.putExtra(KEY_SELECT_EXTRA_MODE, activity.getSelectMode(note)) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt new file mode 100644 index 00000000..4a244b2b --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt @@ -0,0 +1,38 @@ +package com.maubis.scarlet.base.note.folder + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.main.recycler.setFullSpan +import com.maubis.scarlet.base.support.recycler.RecyclerItem +import com.maubis.scarlet.base.support.ui.CircleDrawable + +class SelectorFolderRecyclerHolder(context: Context, view: View) : RecyclerViewHolder(context, view) { + + protected val title: TextView + protected val icon: ImageView + + init { + title = view.findViewById(R.id.folder_title) + icon = view.findViewById(R.id.folder_icon) + } + + override fun populate(itemData: RecyclerItem, extra: Bundle?) { + setFullSpan() + + val item = itemData as SelectorFolderRecyclerItem + title.text = item.title + title.setTextColor(item.titleColor) + title.typeface = FONT_MONSERRAT + title.alpha = 0.8f + + icon.setColorFilter(item.iconColor) + icon.background = CircleDrawable(item.folderColor, false) + icon.alpha = 0.8f + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt new file mode 100644 index 00000000..17b688a8 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt @@ -0,0 +1,25 @@ +package com.maubis.scarlet.base.note.folder + +import android.content.Context +import android.support.v4.content.ContextCompat +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.support.recycler.RecyclerItem +import com.maubis.scarlet.base.support.ui.ColorUtil +import com.maubis.scarlet.base.support.ui.ThemeColorType + + +class SelectorFolderRecyclerItem(context: Context, val folder: Folder) : RecyclerItem() { + + val isLightShaded = ColorUtil.isLightColored(folder.color) + val title = folder.title + val titleColor = instance.themeController().get(ThemeColorType.TERTIARY_TEXT) + + val folderColor = folder.color + val iconColor = when (isLightShaded) { + true -> ContextCompat.getColor(context, R.color.dark_secondary_text) + false -> ContextCompat.getColor(context, R.color.light_primary_text) + } + override val type = Type.FOLDER +} diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt index c9a98bae..bcc9116c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt @@ -9,6 +9,7 @@ import com.maubis.scarlet.base.main.recycler.EmptyRecyclerHolder import com.maubis.scarlet.base.main.recycler.InformationRecyclerHolder import com.maubis.scarlet.base.main.recycler.ToolbarMainRecyclerHolder import com.maubis.scarlet.base.note.folder.FolderRecyclerHolder +import com.maubis.scarlet.base.note.folder.SelectorFolderRecyclerHolder import com.maubis.scarlet.base.note.selection.recycler.SelectableNoteRecyclerViewHolder import com.maubis.scarlet.base.support.recycler.RecyclerItem import java.util.* @@ -70,6 +71,11 @@ fun getSelectableRecyclerItemControllerList( .layoutFile(if (staggered && !isTablet) R.layout.item_note_staggered else R.layout.item_note) .holderClass(SelectableNoteRecyclerViewHolder::class.java) .build()) + list.add(MultiRecyclerViewControllerItem.Builder() + .viewType(RecyclerItem.Type.FOLDER.ordinal) + .layoutFile(R.layout.item_selector_folder) + .holderClass(SelectorFolderRecyclerHolder::class.java) + .build()) list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index e7e5c6af..0ad0013f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -12,13 +12,16 @@ import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.main.recycler.EmptyRecyclerItem +import com.maubis.scarlet.base.note.folder.SelectorFolderRecyclerItem import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.note.recycler.NoteRecyclerItem import com.maubis.scarlet.base.note.recycler.getSelectableRecyclerItemControllerList import com.maubis.scarlet.base.settings.sheet.* +import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -36,20 +39,41 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct notifyThemeChange() setupRecyclerView() - MultiAsyncTask.execute(object : MultiAsyncTask.Task> { - override fun run(): List { + MultiAsyncTask.execute(object : MultiAsyncTask.Task> { + override fun run(): List { val sorting = SortingOptionsBottomSheet.getSortingState() - return sort(getNotes(), sorting) + val notes = sort(getNotes(), sorting) + .sortedBy { it.folder } .map { NoteRecyclerItem(this@SelectableNotesActivityBase, it) } + + if (notes.isEmpty()) { + return notes + } + + val items = emptyList().toMutableList() + var lastFolder = "" + notes.forEach { + val noteFolderId = it.note.folder + if (lastFolder != noteFolderId) { + val folder = instance.foldersDatabase().getByUUID(noteFolderId) + if (folder !== null) { + items.add(SelectorFolderRecyclerItem(this@SelectableNotesActivityBase, folder)) + lastFolder = noteFolderId + } + } + items.add(it) + } + return items } - override fun handle(notes: List) { + override fun handle(notes: List) { adapter.clearItems() if (notes.isEmpty()) { adapter.addItem(EmptyRecyclerItem()) } + var lastFolder = "" notes.forEach { adapter.addItem(it) } diff --git a/base/src/main/res/layout/bottom_sheet_note_options.xml b/base/src/main/res/layout/bottom_sheet_note_options.xml index 256053f9..13dce58c 100644 --- a/base/src/main/res/layout/bottom_sheet_note_options.xml +++ b/base/src/main/res/layout/bottom_sheet_note_options.xml @@ -7,11 +7,11 @@ + android:orientation="vertical"> - + - - - - - - - + + - - + + + + + + + + + + + + + + - - + android:orientation="vertical" + android:padding="@dimen/spacing_normal"> + + + + + + + + + + - - - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0d4dd43c..89c3ad57 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 137 - ext.appconfig_version = '7.0.7' + ext.appconfig_version_code = 139 + ext.appconfig_version = '7.0.9' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From 607de998a7f530ce4f925e43dec3f56920be3242 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 5 Aug 2019 21:43:44 +0100 Subject: [PATCH 059/134] [GDrive] Changing the sync logic to be bound by callable --- .../quicknote/drive/GDriveRemoteDatabase.kt | 68 ++++--------------- .../quicknote/drive/GDriveRemoteFolder.kt | 38 +---------- .../quicknote/drive/GDriveRemoteFolderBase.kt | 3 +- .../drive/GDriveRemoteImageFolder.kt | 18 +---- .../quicknote/drive/GDriveServiceHelper.kt | 21 +++++- 5 files changed, 40 insertions(+), 108 deletions(-) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 173e9162..7b5921d3 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger const val FOLDER_NAME_IMAGES = "images" const val FOLDER_NAME_NOTES = "notes" @@ -95,7 +94,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { private var imageSync: GDriveRemoteImageFolder? = null private var syncing = HashMap() private var syncListener: IPendingUploadListener? = null - private var pendingSyncs: AtomicInteger = AtomicInteger(0) private var databaseUpdateLambda: () -> Unit = { verifyAndNotifyPendingStateChange() } fun init(helper: GDriveServiceHelper) { @@ -120,7 +118,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { it }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() @@ -130,7 +127,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() @@ -140,7 +136,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() @@ -150,7 +145,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { action -> decrementPendingSyncs(action) }, serialiser = { Gson().toJson(it) }, uuidToObject = { ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() @@ -159,8 +153,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { dataType = GDriveDataType.IMAGE, database = gDriveDatabase!!, helper = helper, - onPendingChange = { verifyAndNotifyPendingStateChange() }, - onPendingSyncComplete = { action -> decrementPendingSyncs(action) }) + onPendingChange = { verifyAndNotifyPendingStateChange() }) initRootFolder() } @@ -208,16 +201,12 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } private fun initSubRootFolder(folderName: String, folderId: String) { - val logInfo = "initSubRootFolder($folderName, $folderId)" - log("GDriveRemote", logInfo) - incrementPendingSyncs(logInfo) when (folderName) { FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNote) { GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) } sGDriveFirstSyncNote = true } - decrementPendingSyncs(logInfo) } FOLDER_NAME_NOTES_META -> { notesMetaSync?.initContentFolderId(folderId) { @@ -225,7 +214,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } sGDriveFirstSyncNoteMeta = true } - decrementPendingSyncs(logInfo) } notesMetaSync?.initDeletedFolderId(INVALID_FILE_ID) {} } @@ -234,30 +222,27 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { GlobalScope.launch { resyncDataSync(GDriveDataType.TAG) } sGDriveFirstSyncTag = true } - decrementPendingSyncs(logInfo) } FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncFolder) { GlobalScope.launch { resyncDataSync(GDriveDataType.FOLDER) } sGDriveFirstSyncFolder = true } - decrementPendingSyncs(logInfo) } FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncImage) { GlobalScope.launch { resyncDataSync(GDriveDataType.IMAGE) } sGDriveFirstSyncImage = true } - decrementPendingSyncs(logInfo) } FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) { - decrementPendingSyncs(logInfo) + } FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) { - decrementPendingSyncs(logInfo) + } FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) { - decrementPendingSyncs(logInfo) + } } } @@ -323,24 +308,16 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val pending = database.getAllPending().map { "type=${it.type}, uuid=${it.uuid}, fid=${it.fileId}" }.joinToString(separator = "\n") log("GDrive", "getPendingCount(${database.getPendingCount()})\n$pending") + notifyPendingSyncChange("verifyAndNotifyPendingStateChange") syncListener?.onPendingStateUpdate(currentPendingState) } } - @Synchronized - private fun decrementPendingSyncs(action: String) { - log("GDriveRemote", "pendingSync: decrement to ${pendingSyncs.get() - 1}, action: $action") - if (pendingSyncs.decrementAndGet() <= 0) { - pendingSyncs.set(0) - syncListener?.onPendingSyncsUpdate(false) - } - } - - @Synchronized - private fun incrementPendingSyncs(action: String) { - log("GDriveRemote", "pendingSync: increment to ${pendingSyncs.get() + 1}, action: $action") - if (pendingSyncs.incrementAndGet() >= 1) { - syncListener?.onPendingSyncsUpdate(true) + fun notifyPendingSyncChange(action: String) { + val count = sSyncingCount.get() + when { + count <= 0 -> syncListener?.onPendingSyncsUpdate(false) + count >= 1 -> syncListener?.onPendingSyncsUpdate(true) } } @@ -423,32 +400,31 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } val logInfo = "insert(${type.name}, ${data.uuid})" - incrementPendingSyncs(logInfo) when (type) { GDriveDataType.NOTE -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { notesSync?.insert(data.uuid, this) - } ?: decrementPendingSyncs(logInfo) + } } GDriveDataType.NOTE_META -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { notesMetaSync?.insert(data.uuid, this) - } ?: decrementPendingSyncs(logInfo) + } } GDriveDataType.TAG -> { ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { tagsSync?.insert(this.uuid, this) - } ?: decrementPendingSyncs(logInfo) + } } GDriveDataType.FOLDER -> { ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { foldersSync?.insert(this.uuid, this) - } ?: decrementPendingSyncs(logInfo) + } } GDriveDataType.IMAGE -> { toImageUUID(data.uuid)?.apply { imageSync?.insert(this) - } ?: decrementPendingSyncs(logInfo) + } } } } @@ -460,7 +436,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val logInfo = "remove(${type.name}, ${data.uuid})" log("GDriveRemote", logInfo) - incrementPendingSyncs(logInfo) val uuid = data.uuid when (type) { GDriveDataType.NOTE -> notesSync?.delete(uuid) @@ -471,7 +446,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val imageUUID = toImageUUID(uuid) when { imageUUID !== null -> imageSync?.delete(imageUUID) - else -> decrementPendingSyncs(logInfo) } } } @@ -487,9 +461,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { return } - val logInfo = "onRemoteInsert(${type.name}, ${data.uuid})" - log("GDriveRemote", logInfo) - incrementPendingSyncs(logInfo) when (type) { GDriveDataType.NOTE -> { onRemoteInsertImpl(data.fileId) { @@ -505,7 +476,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) - decrementPendingSyncs(logInfo) } } } @@ -524,7 +494,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) - decrementPendingSyncs(logInfo) } } } @@ -536,7 +505,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) - decrementPendingSyncs(logInfo) } } } @@ -548,7 +516,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) - decrementPendingSyncs(logInfo) } } } @@ -558,7 +525,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) if (imageFile.exists()) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) - decrementPendingSyncs(logInfo) return } @@ -566,7 +532,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { if (it.result == true) { gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) } - decrementPendingSyncs(logInfo) } } } @@ -585,7 +550,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val logInfo = "onRemoteRemove(${type.name}, ${data.uuid})" log("GDriveRemote", logInfo) - incrementPendingSyncs(logInfo) when (type) { GDriveDataType.NOTE -> { IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) @@ -604,7 +568,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } gDriveDbState.remoteDatabaseUpdate(type, data.uuid, databaseUpdateLambda) - decrementPendingSyncs(logInfo) } /** @@ -617,7 +580,6 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { if (data !== null) { onDataAvailable(data) } - decrementPendingSyncs("onRemoteInsertImpl") } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 56dc6cb7..3a1521a5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -13,9 +13,8 @@ class GDriveRemoteFolder( database: GDriveUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit, - onPendingSyncComplete: (String) -> Unit, val serialiser: (T) -> String, - val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { + val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { var networkOrAbsoluteFailure = AtomicBoolean(false) @@ -32,7 +31,6 @@ class GDriveRemoteFolder( val duplicateFilesToDelete: MutableList = emptyList().toMutableList() fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { - val logInfo = "initContentFolderId($fUid)" GlobalScope.launch(Dispatchers.IO) { contentLoading.set(true) contentFolderUid = fUid @@ -59,16 +57,11 @@ class GDriveRemoteFolder( executeDeletePendingActions() } GlobalScope.launch { onLoaded() } - }.addOnFailureListener { - onPendingSyncComplete(logInfo) - }.addOnCanceledListener { - onPendingSyncComplete(logInfo) } } } fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { - val logInfo = "initDeletedFolderId($fUid)" if (fUid == INVALID_FILE_ID) { deletedLoading.set(false) GlobalScope.launch { @@ -90,7 +83,8 @@ class GDriveRemoteFolder( files.forEach { file -> when { localFileIds.containsKey(file.name) -> duplicateFilesToDelete.add(file.id) - getTrueCurrentTime() - (file.modifiedTime?.value ?: 0L) > TimeUnit.DAYS.toMillis(7) -> duplicateFilesToDelete.add(file.id) + getTrueCurrentTime() - (file.modifiedTime?.value + ?: 0L) > TimeUnit.DAYS.toMillis(7) -> duplicateFilesToDelete.add(file.id) else -> { localFileIds[file.name] = file.id notifyDriveData(file, true) @@ -104,10 +98,6 @@ class GDriveRemoteFolder( GlobalScope.launch { executeAllDuplicateDeletion() } GlobalScope.launch { executeDeletePendingActions() } GlobalScope.launch { onLoaded() } - }.addOnFailureListener { - onPendingSyncComplete(logInfo) - }.addOnCanceledListener { - onPendingSyncComplete(logInfo) } } } @@ -151,14 +141,10 @@ class GDriveRemoteFolder( val logInfo = "insert($uuid)" if (deletedLoading.get() || contentLoading.get()) { - if (!contentPendingActions.add(uuid)) { - onPendingSyncComplete(logInfo) - } return } if (networkOrAbsoluteFailure.get()) { - onPendingSyncComplete(logInfo) return } @@ -184,10 +170,7 @@ class GDriveRemoteFolder( if (file !== null) { notifyDriveData(file.id, uuid, timestamp) } - onPendingSyncComplete(logInfo) } - .addOnFailureListener { onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete(logInfo) } return } helper.createFileWithData(contentFolderUid, uuid, data, timestamp) @@ -197,10 +180,7 @@ class GDriveRemoteFolder( contentFiles[uuid] = file.id notifyDriveData(file.id, uuid, timestamp) } - onPendingSyncComplete(logInfo) } - .addOnFailureListener { onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete(logInfo) } } @@ -208,16 +188,11 @@ class GDriveRemoteFolder( * Delete the file on the server based on removal on the local device */ fun delete(uuid: String) { - val logInfo = "delete($uuid)" if (deletedLoading.get() || contentLoading.get()) { - if (!deletedPendingActions.add(uuid)) { - onPendingSyncComplete(logInfo) - } return } if (networkOrAbsoluteFailure.get()) { - onPendingSyncComplete(logInfo) return } @@ -229,7 +204,6 @@ class GDriveRemoteFolder( database.delete(existing) onPendingChange() } - onPendingSyncComplete(logInfo) } return } @@ -238,7 +212,6 @@ class GDriveRemoteFolder( .addOnCompleteListener { contentFiles.remove(uuid) if (deletedFolderUid == INVALID_FILE_ID) { - onPendingSyncComplete(logInfo) return@addOnCompleteListener } @@ -252,13 +225,8 @@ class GDriveRemoteFolder( deletedFiles[uuid] = file.id notifyDriveData(file.id, uuid, timestamp, true) } - onPendingSyncComplete(logInfo) } - .addOnFailureListener { onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete(logInfo) } } } - .addOnFailureListener { onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete(logInfo) } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 970f4848..00ad7143 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -12,8 +12,7 @@ abstract class GDriveRemoteFolderBase( val dataType: GDriveDataType, val database: GDriveUploadDataDao, val helper: GDriveServiceHelper, - val onPendingChange: () -> Unit, - val onPendingSyncComplete: (String) -> Unit) { + val onPendingChange: () -> Unit) { protected fun notifyDriveData(file: File, deleted: Boolean = false) { val modifiedTime = file.modifiedTime?.value ?: file.modifiedByMeTime?.value ?: 0L diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 0b7851d2..341bdbfe 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -41,8 +41,7 @@ class GDriveRemoteImageFolder( dataType: GDriveDataType, database: GDriveUploadDataDao, helper: GDriveServiceHelper, - onPendingChange: () -> Unit, - onPendingSyncComplete: (String) -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange, onPendingSyncComplete) { + onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { var networkOrAbsoluteFailure = AtomicBoolean(false) @@ -54,7 +53,6 @@ class GDriveRemoteImageFolder( val deletedPendingActions = emptySet().toMutableSet() fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { - val logInfo = "initContentFolderId($fUid)" contentFolderUid = fUid GlobalScope.launch(Dispatchers.IO) { helper.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { @@ -72,10 +70,6 @@ class GDriveRemoteImageFolder( contentLoading.set(false) } GlobalScope.launch { onLoaded() } - }.addOnFailureListener { - onPendingSyncComplete(logInfo) - }.addOnCanceledListener { - onPendingSyncComplete(logInfo) } } } @@ -89,7 +83,6 @@ class GDriveRemoteImageFolder( } if (networkOrAbsoluteFailure.get()) { - onPendingSyncComplete(logInfo) return } @@ -101,7 +94,6 @@ class GDriveRemoteImageFolder( save(database) } onPendingChange() - onPendingSyncComplete(logInfo) } return } @@ -117,10 +109,7 @@ class GDriveRemoteImageFolder( contentFiles[id] = file.id notifyDriveData(file.id, gDriveUUID, timestamp) } - onPendingSyncComplete(logInfo) } - .addOnFailureListener { onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete(logInfo) } } fun delete(id: ImageUUID) { @@ -131,7 +120,6 @@ class GDriveRemoteImageFolder( } if (networkOrAbsoluteFailure.get()) { - onPendingSyncComplete(logInfo) return } @@ -141,7 +129,6 @@ class GDriveRemoteImageFolder( if (existing !== null) { database.delete(existing) } - onPendingSyncComplete(logInfo) } return } @@ -155,10 +142,7 @@ class GDriveRemoteImageFolder( .addOnCompleteListener { notifyDriveData(fuid, id.name(), timestamp, true) contentFiles.remove(id) - onPendingSyncComplete(logInfo) } - .addOnFailureListener { onPendingSyncComplete(logInfo) } - .addOnCanceledListener { onPendingSyncComplete(logInfo) } } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index f0bef944..0934bc0d 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote.drive import android.os.SystemClock +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.api.client.http.ByteArrayContent @@ -33,6 +34,24 @@ const val MIN_RESET_QUERIES_PER_SECOND = 0.1 var lastCheckpointTime: AtomicLong = AtomicLong(0L) var numQueriesSinceLastCheckpoint: AtomicLong = AtomicLong(0L) +var sSyncingCount: AtomicLong = AtomicLong(0) + +class CountingErrorCallable(val action: String, callable: Callable) : Callable { + val errorCallable = ErrorCallable(action, callable) + + override fun call(): T? { + try { + sSyncingCount.incrementAndGet() + gDrive?.notifyPendingSyncChange(action) + return errorCallable.call() + } catch (exception: Exception) { + return throwOrReturn(exception, null) + } finally { + sSyncingCount.decrementAndGet() + gDrive?.notifyPendingSyncChange(action) + } + } +} class ErrorCallable(val action: String, val callable: Callable) : Callable { private var delay: Long = 200L @@ -89,7 +108,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) { private val mExecutor = Executors.newFixedThreadPool(4) fun execute(action: String = "", callable: Callable): Task { - return Tasks.call(mExecutor, ErrorCallable(action, callable)) + return Tasks.call(mExecutor, CountingErrorCallable(action, callable)) } fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { From 61fee393a81ba40d1e5128e35d5f3b4851b2d60d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 5 Aug 2019 22:36:04 +0100 Subject: [PATCH 060/134] [Widget] Added settings for widget colors --- .../settings/sheet/ColorPickerBottomSheet.kt | 6 +- .../scarlet/base/support/ui/ColorUtil.kt | 3 + .../base/widget/AllNotesWidgetProvider.kt | 6 + .../widget/sheet/WidgetOptionsBottomSheet.kt | 66 ++++++++++- base/src/main/res/drawable/icon_no_color.png | Bin 0 -> 685 bytes .../res/layout/widget_layout_all_notes.xml | 107 ++++++++++-------- base/src/main/res/values/strings.xml | 8 ++ 7 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 base/src/main/res/drawable/icon_no_color.png diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt index 8721b33d..19b399c0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt @@ -34,7 +34,11 @@ object ColorPickerItemSpec { val row = Row.create(context) .alignItems(YogaAlign.CENTER) .child(RoundIcon.create(context) - .iconRes(if (isSelected) R.drawable.ic_done_white_48dp else R.drawable.ic_empty) + .iconRes(when { + isSelected -> R.drawable.ic_done_white_48dp + color == Color.TRANSPARENT -> R.drawable.icon_no_color + else -> R.drawable.ic_empty + }) .bgColor(color) .showBorder(true) .iconColorRes(if (ColorUtil.isLightColored(color)) R.color.dark_tertiary_text else R.color.light_secondary_text) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ColorUtil.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ColorUtil.kt index 3f1855b8..79b27db8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ColorUtil.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ColorUtil.kt @@ -6,6 +6,9 @@ import android.support.v4.graphics.ColorUtils object ColorUtil { fun isLightColored(color: Int): Boolean { + if (Color.alpha(color) < 100) { + return true + } return ColorUtils.calculateLuminance(color) > 0.4 } diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt index 905526f4..72b3941a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt @@ -14,6 +14,9 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.note.creation.activity.CreateListNoteActivity import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity +import com.maubis.scarlet.base.support.ui.visibility +import com.maubis.scarlet.base.widget.sheet.sWidgetBackgroundColor +import com.maubis.scarlet.base.widget.sheet.sWidgetShowToolbar const val STORE_KEY_ALL_NOTE_WIDGET = "all_note_widget" @@ -34,6 +37,9 @@ class AllNotesWidgetProvider : AppWidgetProvider() { intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)) views.setRemoteAdapter(R.id.list, intent) + views.setInt(R.id.widget_background, "setBackgroundColor", sWidgetBackgroundColor) + views.setViewVisibility(R.id.toolbar, visibility(sWidgetShowToolbar)) + val noteIntent = Intent(context, ViewAdvancedNoteActivity::class.java) noteIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]) val notePendingIntent = PendingIntent.getActivity(context, 0, noteIntent, PendingIntent.FLAG_UPDATE_CURRENT) diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index 95a5303f..4231734f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -5,6 +5,7 @@ import android.app.Dialog import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Intent +import android.graphics.Color import com.facebook.litho.ComponentContext import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity @@ -14,11 +15,13 @@ import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.getFullText import com.maubis.scarlet.base.note.getFullTextForDirectMarkdownRender +import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet +import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.widget.AllNotesWidgetProvider import com.maubis.scarlet.base.widget.NoteWidgetProvider import kotlinx.coroutines.GlobalScope @@ -45,6 +48,16 @@ var sWidgetShowDeletedNotes: Boolean get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, false) set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, value) +const val STORE_KEY_WIDGET_BACKGROUND_COLOR = "widget_background_color" +var sWidgetBackgroundColor: Int + get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_BACKGROUND_COLOR, 0x65000000) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_BACKGROUND_COLOR, value) + +const val STORE_KEY_WIDGET_SHOW_TOOLBAR = "widget_show_toolbar" +var sWidgetShowToolbar: Boolean + get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_TOOLBAR, true) + set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_TOOLBAR, value) + fun getWidgetNoteText(note: Note): CharSequence { if (note.locked && !sWidgetShowLockedNotes) { return "******************\n***********\n****************" @@ -125,12 +138,42 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { isSelectable = true, selected = sWidgetShowDeletedNotes )) + options.add(LithoOptionsItem( + title = R.string.widget_option_show_toolbar, + subtitle = R.string.widget_option_show_toolbar_details, + icon = R.drawable.ic_action_grid, + listener = { + sWidgetShowToolbar = !sWidgetShowToolbar + notifyAllNotesConfigChanged(activity) + reset(activity, dialog) + }, + isSelectable = true, + selected = sWidgetShowToolbar + )) + options.add(LithoOptionsItem( + title = R.string.widget_option_background_color, + subtitle = R.string.widget_option_background_color_details, + icon = R.drawable.ic_action_color, + listener = { + openSheet(activity, ColorPickerBottomSheet().apply { + config = ColorPickerDefaultController( + title = R.string.widget_option_background_color, + selectedColor = sWidgetBackgroundColor, + colors = listOf(intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.LTGRAY, 0x65000000, Color.DKGRAY, Color.BLACK)), + onColorSelected = { + sWidgetBackgroundColor = it + notifyAllNotesConfigChanged(activity) + }, + columns = 6) + }) + } + )) return options } fun notifyWidgetConfigChanged(activity: MainActivity) { GlobalScope.launch { - val broadcastIntent = GlobalScope.async { + val singleNoteBroadcastIntent = GlobalScope.async { val application: Application = activity.applicationContext as Application val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( ComponentName(application, NoteWidgetProvider::class.java)) @@ -142,7 +185,24 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { } AllNotesWidgetProvider.notifyAllChanged(activity) - activity.sendBroadcast(broadcastIntent.await()) + activity.sendBroadcast(singleNoteBroadcastIntent.await()) + } + } + + fun notifyAllNotesConfigChanged(activity: MainActivity) { + GlobalScope.launch { + val allNotesBroadcastIntent = GlobalScope.async { + val application: Application = activity.applicationContext as Application + val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( + ComponentName(application, AllNotesWidgetProvider::class.java)) + + val intent = Intent(application, AllNotesWidgetProvider::class.java) + intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + intent + } + + activity.sendBroadcast(allNotesBroadcastIntent.await()) } } } \ No newline at end of file diff --git a/base/src/main/res/drawable/icon_no_color.png b/base/src/main/res/drawable/icon_no_color.png new file mode 100644 index 0000000000000000000000000000000000000000..39345156aa8ecb9707acb15ab0dedb1867cea920 GIT binary patch literal 685 zcmV;e0#f~nP)DLjeJh5WzXb-=ZJYZ~(YYpdYUE6Wu7!2LRUz zWW|9i4swZ3l;k~t>jc7a5qDf8oan=P0Cy6YiM!^DKK#c{0vaa)&>HuZGdEC*cMG4f85t zgXl+Y0G}n~6y4YXK1?|1|M_>F;3PPOL?^a^#|d?DhLCeWT%6$J1Sd&F7kUHuJYkaP z!X;3U@Qi;%2l4?ZNhl>cFcB0bEEF9$hiCe`o$!>uMK$8%xqfaZWERzEjyJlynb2HR zV*w~km@lfa2NWh86V*5i3KK4iYPA#`KwTd=XmQLMOo|rgw$Nd8@q`m!apG)AspR! zh$kM?PQG%HpfjQ)cW}o`h7wV}3p$~N=*Isv;0!@~#}WDyT>@~Cpcl!g#RQJ=mhYah zjouU?v;^QHA-xzN=!7Qnoq-9?@547U!AV^)6aTR6`|u6LUlbM{EX%Si%d$QKK$)Ai Th&&CZ00000NkvXXu0mjfI43W( literal 0 HcmV?d00001 diff --git a/base/src/main/res/layout/widget_layout_all_notes.xml b/base/src/main/res/layout/widget_layout_all_notes.xml index 62c702a5..3bfe5fc9 100644 --- a/base/src/main/res/layout/widget_layout_all_notes.xml +++ b/base/src/main/res/layout/widget_layout_all_notes.xml @@ -2,56 +2,67 @@ + android:background="@color/transparent" + android:orientation="vertical"> - - - - - - - - - + android:layout_height="match_parent" + android:background="@color/transparent" + android:orientation="vertical" + android:paddingTop="2dp" + android:paddingBottom="2dp"> - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9ec99895..0de80456 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -361,6 +361,14 @@ Show Notes in Trash Allow notes in trash to be shown in home screen widgets + + Widget Background + Choose the background color for the multi-notes widget + + Show Action Toolbar + Multi-notes widget shows the toolbar + + Help and Common Questions From a578f978a7aa07e8dc9eb14269d32cc85bedb708 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 6 Aug 2019 00:09:18 +0100 Subject: [PATCH 061/134] Setting up proper logout and clear API --- app/build.gradle | 4 +- .../base/widget/AllNotesWidgetService.kt | 2 +- build.gradle | 4 +- .../activity/FirebaseRemovalActivity.kt | 93 +++++++++++++------ .../firebase/activity/ForgetMeActivity.kt | 42 ++++++--- .../quicknote/scarlet/ScarletAuthenticator.kt | 2 +- 6 files changed, 100 insertions(+), 47 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f0d58fcd..cd6e63c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 139 - versionName '7.0.9' + versionCode 140 + versionName '7.0.10' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt index 1b24a47c..307dea90 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt @@ -34,7 +34,7 @@ class AllNotesRemoteViewsFactory(val context: Context) : RemoteViewsService.Remo } override fun getItemId(position: Int): Long { - return if(position >= notes.size) notes[position].uid.toLong() else 0 + return if(position < notes.size) notes[position].uid.toLong() else 0 } override fun onDataSetChanged() { diff --git a/build.gradle b/build.gradle index 89c3ad57..1d90828e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 139 - ext.appconfig_version = '7.0.9' + ext.appconfig_version_code = 140 + ext.appconfig_version = '7.0.10' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt index e93a9501..a49c1d45 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt @@ -3,39 +3,27 @@ package com.bijoysingh.quicknote.firebase.activity import android.content.Context import android.content.Intent import android.os.Bundle -import android.util.Log +import android.os.Handler import com.bijoysingh.quicknote.R -import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.hasAcceptedThePolicy -import com.bijoysingh.quicknote.firebase.initFirebaseDatabase +import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity.Companion.firebaseForgetMe +import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity.Companion.forgettingInProcess import com.bijoysingh.quicknote.scarlet.sFirebaseKilled -import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView -import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.ToastHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount -import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.GoogleApiClient import com.google.android.gms.common.api.Scope -import com.google.android.gms.tasks.OnCompleteListener -import com.google.android.gms.tasks.Task import com.google.api.services.drive.DriveScopes -import com.google.firebase.auth.AuthResult -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseUser -import com.google.firebase.auth.GoogleAuthProvider import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance -import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN import com.maubis.scarlet.base.support.ui.ThemedActivity -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean -class FirebaseRemovalActivity : ThemedActivity() { +class FirebaseRemovalActivity : ThemedActivity(), GoogleApiClient.OnConnectionFailedListener { lateinit var context: Context lateinit var component: Component @@ -49,28 +37,77 @@ class FirebaseRemovalActivity : ThemedActivity() { componentContext = ComponentContext(context) setButton(false) notifyThemeChange() + setupGoogleLogin() } private fun setButton(state: Boolean) { loggingIn.set(state) component = FirebaseRemovalRootView.create(componentContext) .onClick { - // TODO: Delete remote data... - setButton(true) - GlobalScope.launch { - instance.authenticator().logout() - setButton(false) - - sFirebaseKilled = true - instance.authenticator().openLoginActivity(context)?.run() - finish() + if (loggingIn.get()) { + return@onClick } + + setButton(true) + forgettingInProcess = true + firebaseForgetMe( + onComplete = { + signOut { + forgettingInProcess = false + instance.authenticator().openLoginActivity(context)?.run() + finish() + } + }, + onFailure = { + forgettingInProcess = false + ToastHelper.show(context, "Failed logging you out. Try logging in again.") + context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) + finish() + }) } .removingItems(state) .build() setContentView(LithoView.create(componentContext, component)) } + lateinit var mGoogleApiClient: GoogleApiClient + private fun setupGoogleLogin() { + val gso = GoogleSignInOptions.Builder( + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() + + mGoogleApiClient = GoogleApiClient + .Builder(this) + .enableAutoManage(this, this) + .addApi(Auth.GOOGLE_SIGN_IN_API, gso) + .build() + } + + private fun signOut(onSuccess: () -> Unit) { + if (mGoogleApiClient.isConnecting) { + Handler().postDelayed({ + signOut(onSuccess) + }, 500) + return + } + + if (mGoogleApiClient.isConnected) { + Auth.GoogleSignInApi.signOut(mGoogleApiClient).addStatusListener { + onSuccess() + } + return + } + + ToastHelper.show(context, "Failed logging you out properly. Try again later.") + } + + override fun onConnectionFailed(p0: ConnectionResult) { + ToastHelper.show(context, "Failed logging you out properly. Try again later.") + } + + override fun onBackPressed() { super.onBackPressed() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index 365af653..f71293db 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -7,6 +7,9 @@ import android.widget.CheckBox import android.widget.TextView import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.firebase +import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.scarlet.sFirebaseKilled +import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.github.bijoysingh.starter.util.ToastHelper import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount @@ -57,19 +60,15 @@ class ForgetMeActivity : ThemedActivity() { } forgettingInProcess = true - firebase?.deleteEverything() - - FirebaseAuth.getInstance().currentUser - ?.delete() - ?.addOnCompleteListener { - if (it.isSuccessful) { - ApplicationBase.instance.authenticator().logout() - finish() - return@addOnCompleteListener - } - + firebaseForgetMe( + onComplete = { + sFirebaseKilled = true + finish() + }, + onFailure = { reauthAndDelete() - } + }) + } cancelBtn.setOnClickListener { finish() @@ -82,6 +81,24 @@ class ForgetMeActivity : ThemedActivity() { companion object { var forgettingInProcess = false + + fun firebaseForgetMe(onComplete: () -> Unit = {}, onFailure: () -> Unit = {}) { + firebase?.deleteEverything() + + FirebaseAuth.getInstance().currentUser + ?.delete() + ?.addOnCompleteListener { + if (it.isSuccessful) { + firebase?.logout() + gDrive?.logout() + sFirebaseKilled = true + sGDriveLoggedIn = false + onComplete() + return@addOnCompleteListener + } + onFailure() + } + } } @@ -150,5 +167,4 @@ class ForgetMeActivity : ThemedActivity() { } } - } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 76e562d2..7321bc5c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -22,7 +22,7 @@ var sGDriveLoggedIn: Boolean get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit -const val KEY_FIREBASE_KILLED = "firebase_killed" +const val KEY_FIREBASE_KILLED = "firebase_killed_v2" var sFirebaseKilled: Boolean get() = Scarlet.gDriveConfig?.get(KEY_FIREBASE_KILLED, false) ?: false set(value) = Scarlet.gDriveConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit From 3259988b2fc00b55acd0a5d42d24cf348a12e2b9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 6 Aug 2019 21:39:14 +0100 Subject: [PATCH 062/134] [GDrive] Adding Pending Bottom Sheet --- app/build.gradle | 4 +- .../com/maubis/scarlet/base/MainActivity.kt | 5 + .../base/config/auth/IAuthenticator.kt | 3 + .../base/config/auth/NullAuthenticator.kt | 4 + .../main/specs/MainActivityBottomBarSpec.kt | 14 +- .../res/drawable/pending_note_background.xml | 9 + base/src/main/res/values/strings.xml | 7 + build.gradle | 4 +- .../drive/GDrivePendingBottomSheet.kt | 272 ++++++++++++++++++ .../quicknote/scarlet/ScarletAuthenticator.kt | 13 +- 10 files changed, 325 insertions(+), 10 deletions(-) create mode 100644 base/src/main/res/drawable/pending_note_background.xml create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt diff --git a/app/build.gradle b/app/build.gradle index cd6e63c6..3c6e43d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 140 - versionName '7.0.10' + versionCode 141 + versionName '7.0.11' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 587cc7f3..2e7696f2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -367,6 +367,11 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { instance.authenticator().requestSync(true) } } + .onLongClick { + if (!lastSyncHappening.get()) { + instance.authenticator().showPendingSync(this@MainActivity) + } + } .build())) if (!isSyncHappening && isSyncPending) { instance.authenticator().requestSync(false) diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt index 5a74f87a..5cf57465 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/IAuthenticator.kt @@ -1,6 +1,7 @@ package com.maubis.scarlet.base.config.auth import android.content.Context +import com.maubis.scarlet.base.support.ui.ThemedActivity interface IAuthenticator { @@ -22,6 +23,8 @@ interface IAuthenticator { fun setPendingUploadListener(listener: IPendingUploadListener?) + fun showPendingSync(activity: ThemedActivity) + fun requestSync(forced: Boolean) fun logout() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt index 6eceb255..ce66c911 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/auth/NullAuthenticator.kt @@ -1,8 +1,10 @@ package com.maubis.scarlet.base.config.auth import android.content.Context +import com.maubis.scarlet.base.support.ui.ThemedActivity class NullAuthenticator : IAuthenticator { + override fun openLoginActivity(context: Context): Runnable? = null override fun openForgetMeActivity(context: Context): Runnable? = null @@ -24,4 +26,6 @@ class NullAuthenticator : IAuthenticator { override fun setPendingUploadListener(listener: IPendingUploadListener?) {} override fun requestSync(forced: Boolean) {} + + override fun showPendingSync(activity: ThemedActivity) {} } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 6245d9db..861ecf19 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -24,7 +24,10 @@ import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.note.folder.sheet.CreateOrEditFolderBottomSheet import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.specs.* +import com.maubis.scarlet.base.support.specs.EmptySpec +import com.maubis.scarlet.base.support.specs.ToolbarColorConfig +import com.maubis.scarlet.base.support.specs.bottomBarCard +import com.maubis.scarlet.base.support.specs.bottomBarRoundIcon import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.support.ui.ThemeColorType import kotlinx.coroutines.Dispatchers @@ -214,8 +217,9 @@ object MainActivitySyncingNowSpec { .alignContent(YogaAlign.CENTER) .paddingDip(YogaEdge.VERTICAL, 8f) .paddingDip(YogaEdge.HORIZONTAL, 12f) - .backgroundRes(R.drawable.login_button_disabled) + .backgroundRes(R.drawable.secondary_rounded_bg) .clickHandler(MainActivitySyncingNow.onClickEvent(context)) + .longClickHandler(MainActivitySyncingNow.onLongClickEvent(context)) .child(syncIcon) .child(Text.create(context) .typeface(FONT_MONSERRAT) @@ -229,4 +233,10 @@ object MainActivitySyncingNowSpec { fun onClickEvent(context: ComponentContext, @Prop onClick: () -> Unit) { onClick() } + + @OnEvent(LongClickEvent::class) + fun onLongClickEvent(context: ComponentContext, @Prop onLongClick: () -> Unit): Boolean { + onLongClick() + return true + } } diff --git a/base/src/main/res/drawable/pending_note_background.xml b/base/src/main/res/drawable/pending_note_background.xml new file mode 100644 index 00000000..6f09de78 --- /dev/null +++ b/base/src/main/res/drawable/pending_note_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 0de80456..40475775 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -376,6 +376,13 @@ Syncing Pending Backup + Local + Remote + + Unavailable + Created + Deleted + Updated Developer Options diff --git a/build.gradle b/build.gradle index 1d90828e..e3b6527d 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 140 - ext.appconfig_version = '7.0.10' + ext.appconfig_version_code = 141 + ext.appconfig_version = '7.0.11' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt new file mode 100644 index 00000000..d8713649 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -0,0 +1,272 @@ +package com.bijoysingh.quicknote.drive + +import android.app.Dialog +import android.graphics.drawable.Drawable +import android.text.TextUtils +import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.GDriveUploadData +import com.bijoysingh.quicknote.database.gDriveDatabase +import com.facebook.litho.* +import com.facebook.litho.annotations.* +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.SolidColor +import com.facebook.litho.widget.Text +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.github.bijoysingh.starter.util.DateFormatter +import com.google.gson.Gson +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.export.data.getExportableNoteMeta +import com.maubis.scarlet.base.note.getFullText +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.specs.color +import com.maubis.scarlet.base.support.ui.ThemeColorType +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean + +data class PendingItem( + val state: GDriveUploadData, + val info: String? +) + +@LayoutSpec +object PendingItemIconSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop(resType = ResType.STRING) label: String, + @Prop(resType = ResType.DRAWABLE) icon: Drawable): Component { + val secondaryColor = instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + return Row.create(context) + .paddingDip(YogaEdge.HORIZONTAL, 8f) + .paddingDip(YogaEdge.VERTICAL, 4f) + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .backgroundRes(R.drawable.secondary_rounded_bg) + .child(Image.create(context) + .drawable(icon.color(secondaryColor)) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .heightDip(24f)) + .child(Text.create(context) + .text(label) + .textSizeRes(R.dimen.font_size_xsmall) + .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .textColor(secondaryColor)) + .build() + } +} + +@LayoutSpec +object PendingItemLayoutSpec { + @OnCreateLayout + fun onCreate(context: ComponentContext, + @Prop option: PendingItem): Component { + val theme = ApplicationBase.instance.themeController() + val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) + val subtitleColor = theme.get(ThemeColorType.TERTIARY_TEXT) + val hintColor = theme.get(ThemeColorType.HINT_TEXT) + + val uuid = option.state.uuid + val info = option.info ?: "N/A" + val icon = when (option.state.type) { + GDriveDataType.NOTE.name -> R.drawable.ic_note_white_48dp + GDriveDataType.NOTE_META.name -> R.drawable.ic_info + GDriveDataType.TAG.name -> R.drawable.ic_action_tags + GDriveDataType.FOLDER.name -> R.drawable.ic_folder + GDriveDataType.IMAGE.name -> R.drawable.ic_image_gallery + else -> R.drawable.ic_action_lock + } + val label = when (option.state.type) { + GDriveDataType.NOTE.name -> "Note" + GDriveDataType.NOTE_META.name -> "Info" + GDriveDataType.TAG.name -> "Tag" + GDriveDataType.FOLDER.name -> "Folder" + GDriveDataType.IMAGE.name -> "Image" + else -> "Invalid" + } + val localState = when { + option.state.lastUpdateTimestamp == 0L -> R.string.pending_backup_state_unavailable + option.state.gDriveUpdateTimestamp == 0L && !option.state.localStateDeleted -> R.string.pending_backup_state_created + option.state.localStateDeleted -> R.string.pending_backup_state_deleted + else -> R.string.pending_backup_state_updated + } + val localUpdateTime = when { + option.state.lastUpdateTimestamp == 0L -> "" + else -> DateFormatter.getDate(DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, option.state.lastUpdateTimestamp) + } + + val remoteState = when { + option.state.gDriveUpdateTimestamp == 0L -> R.string.pending_backup_state_unavailable + option.state.lastUpdateTimestamp == 0L && !option.state.gDriveStateDeleted -> R.string.pending_backup_state_created + option.state.gDriveStateDeleted -> R.string.pending_backup_state_deleted + else -> R.string.pending_backup_state_updated + } + val remoteUpdateTime = when { + option.state.gDriveUpdateTimestamp == 0L -> "" + else -> DateFormatter.getDate(DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, option.state.gDriveUpdateTimestamp) + } + + val column = Column.create(context) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + Row.create(context) + .paddingDip(YogaEdge.ALL, 2f) + .alignItems(YogaAlign.CENTER) + .child( + PendingItemIcon.create(context) + .iconRes(icon) + .label(label) + .marginDip(YogaEdge.END, 16f)) + .child( + Text.create(context) + .text(uuid) + .maxLines(1) + .ellipsize(TextUtils.TruncateAt.END) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .textColor(subtitleColor))) + .child( + Text.create(context) + .text(info) + .maxLines(5) + .paddingDip(YogaEdge.ALL, 8f) + .marginDip(YogaEdge.ALL, 8f) + .ellipsize(TextUtils.TruncateAt.MIDDLE) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_MONSERRAT) + .backgroundRes(R.drawable.pending_note_background) + .textColor(subtitleColor)) + .child( + Row.create(context) + .widthPercent(100f) + .paddingDip(YogaEdge.ALL, 4f) + .child(Text.create(context) + .textRes(R.string.pending_backup_local_state) + .textSizeRes(R.dimen.font_size_small) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .textColor(hintColor)) + .child(Text.create(context) + .textRes(localState) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + .child(Text.create(context) + .text(localUpdateTime) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + ) + .child( + Row.create(context) + .widthPercent(100f) + .paddingDip(YogaEdge.ALL, 4f) + .child(Text.create(context) + .textRes(R.string.pending_backup_remote_state) + .textSizeRes(R.dimen.font_size_small) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .textColor(hintColor)) + .child(Text.create(context) + .textRes(remoteState) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + .child(Text.create(context) + .text(remoteUpdateTime) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + ) + .child(SolidColor.create(context) + .color(hintColor) + .heightDip(0.5f) + .widthDip(196f) + .alignSelf(YogaAlign.CENTER) + .marginDip(YogaEdge.TOP, 12f) + .alpha(0.4f)) + return column.build() + } + + @OnEvent(ClickEvent::class) + fun onItemClick(context: ComponentContext, @Prop onClick: () -> Unit) { + onClick() + } +} + +class GDrivePendingBottomSheet : LithoBottomSheet() { + + val dataAvailable: AtomicBoolean = AtomicBoolean(false) + val data: MutableList = emptyList().toMutableList() + fun requestData(onDataAvailable: () -> Unit) { + GlobalScope.launch { + data.clear() + gDriveDatabase?.getAllPending()?.forEach { + val pendingItem = when (it.type) { + GDriveDataType.NOTE.name -> PendingItem(state = it, info = instance.notesDatabase().getByUUID(it.uuid)?.getFullText()) + GDriveDataType.NOTE_META.name -> { + val note = instance.notesDatabase().getByUUID(it.uuid)?.getExportableNoteMeta() + when { + (note == null) -> PendingItem(state = it, info = null) + else -> PendingItem(state = it, info = Gson().toJson(note)) + } + } + GDriveDataType.TAG.name -> PendingItem(state = it, info = instance.tagsDatabase().getByUUID(it.uuid)?.title) + GDriveDataType.FOLDER.name -> PendingItem(state = it, info = instance.foldersDatabase().getByUUID(it.uuid)?.title) + GDriveDataType.IMAGE.name -> PendingItem(state = it, info = "Image") + else -> null + } + if (pendingItem !== null) { + data.add(pendingItem) + } + } + dataAvailable.set(true) + GlobalScope.launch(Main) { + onDataAvailable() + } + } + } + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + try { + val component = Column.create(componentContext) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child(getLithoBottomSheetTitle(componentContext) + .textRes(R.string.home_pending_backup_top_layout) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + data.forEach { + component.child( + PendingItemLayout.create(componentContext) + .option(it) + .onClick {}) + } + return component.build() + } finally { + if (!dataAvailable.get()) { + requestData { reset(componentContext.androidContext, dialog) } + } + } + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 7321bc5c..739a246c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -5,15 +5,14 @@ import android.content.Intent import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.gDriveDatabase -import com.bijoysingh.quicknote.drive.GDriveAuthenticator -import com.bijoysingh.quicknote.drive.GDriveLoginActivity -import com.bijoysingh.quicknote.drive.GDriveLogoutActivity -import com.bijoysingh.quicknote.drive.GDriveRemoteDatabaseState +import com.bijoysingh.quicknote.drive.* import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.auth.IPendingUploadListener +import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.ui.ThemedActivity import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -95,4 +94,10 @@ class ScarletAuthenticator() : IAuthenticator { override fun openLogoutActivity(context: Context) = Runnable { context.startActivity(Intent(context, GDriveLogoutActivity::class.java)) } + + override fun showPendingSync(activity: ThemedActivity) { + if (shouldIgnoreFirebase()) { + openSheet(activity, GDrivePendingBottomSheet()) + } + } } \ No newline at end of file From fda7dde92057e631a2f72cef87b4e8735cd9a8cb Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 6 Aug 2019 22:21:33 +0100 Subject: [PATCH 063/134] Fixing problems around broken imports, --- .../quicknote/drive/GDriveRemoteDatabase.kt | 27 ++++++++++------- .../drive/GDriveRemoteDatabaseState.kt | 30 ++++++++++++++++++- .../quicknote/scarlet/ScarletFolderActor.kt | 5 +++- .../quicknote/scarlet/ScarletNoteActor.kt | 8 ++--- .../quicknote/scarlet/ScarletTagActor.kt | 5 +++- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 7b5921d3..acf3cb21 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -158,6 +158,10 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { initRootFolder() } + fun isValid(): Boolean { + return isValidController + } + fun reset() { isValidController = false driveHelper = null @@ -370,21 +374,22 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { continue } - log("GDrive", "resyncDataSync(${type.name}, ${pendingItem.uuid}, ${pendingItem.lastUpdateTimestamp}, ${pendingItem.gDriveUpdateTimestamp})") val sameDelete = pendingItem.localStateDeleted == pendingItem.gDriveStateDeleted val localDeleted = pendingItem.localStateDeleted val remoteDeleted = pendingItem.gDriveStateDeleted val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.gDriveUpdateTimestamp - if (!sameUpdateTime || !sameUpdateTime) { - when { - !sameDelete && remoteDeleted && !sameUpdateTime -> onRemoteRemove(type, pendingItem) // Remote removal more takes precedence to local updates - !sameDelete && localDeleted && !sameUpdateTime -> remove(type, pendingItem) // Local removal more takes precedence to remote updates - sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp && localDeleted -> remove(type, pendingItem) - sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp -> insert(type, pendingItem) - sameDelete && pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp && localDeleted -> onRemoteRemove(type, pendingItem) - sameDelete && pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp -> onRemoteInsert(type, pendingItem) - !sameDelete && sameUpdateTime -> gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) // Ignoring - } + val isLocalMoreRecent = pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp + val isRemoteMoreRecent = pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp + when { + sameUpdateTime -> gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) + !sameDelete && isRemoteMoreRecent && remoteDeleted -> onRemoteRemove(type, pendingItem) + !sameDelete && isLocalMoreRecent && localDeleted -> remove(type, pendingItem) + !sameDelete && isLocalMoreRecent && remoteDeleted -> insert(type, pendingItem) + !sameDelete && isRemoteMoreRecent && localDeleted -> onRemoteInsert(type, pendingItem) + sameDelete && isLocalMoreRecent && localDeleted -> remove(type, pendingItem) + sameDelete && isLocalMoreRecent && !localDeleted -> insert(type, pendingItem) + sameDelete && isRemoteMoreRecent && localDeleted -> onRemoteRemove(type, pendingItem) + sameDelete && isRemoteMoreRecent && !localDeleted -> onRemoteInsert(type, pendingItem) } } syncing[type]?.set(false) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt index 7a0c90f8..2943839f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt @@ -16,7 +16,7 @@ import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { +class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { init { gDriveDatabase = genGDriveUploadDatabase(context) @@ -108,6 +108,34 @@ class GDriveRemoteDatabaseState(context: Context): IRemoteDatabaseState { } } + /** + * Deletes the item from the database, to allow for deletion + * in case the user logs out and the data is deleted + */ + fun stopTrackingItem(data: Any, onExecution: () -> Unit) { + val removeFromDatabase = { itemType: GDriveDataType, itemUUID: String -> + GlobalScope.launch { + val database = gDriveDatabase + if (database !== null) { + val existing = database.getByUUID(itemType.name, itemUUID) + if (existing !== null) { + database.delete(existing) + } + } + onExecution() + } + } + + when { + data is Tag -> removeFromDatabase(GDriveDataType.TAG, data.uuid) + data is Folder -> removeFromDatabase(GDriveDataType.FOLDER, data.uuid) + data is Note -> { + removeFromDatabase(GDriveDataType.NOTE, data.uuid) + removeFromDatabase(GDriveDataType.NOTE_META, data.uuid) + } + else -> maybeThrow("removeFromDb called with unhandled data type") + } + } /** * Notifies that an attempt to update this item was made. diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index a9fe0737..fc8bb008 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -18,6 +18,9 @@ class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { override fun delete() { super.delete() - gDriveDbState?.notifyRemove(folder, notifyChange) + when { + gDrive?.isValid() == true -> gDriveDbState?.notifyRemove(folder, notifyChange) + else -> gDriveDbState?.stopTrackingItem(folder, notifyChange) + } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index 5b683166..784ccff9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -1,11 +1,8 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context -import com.bijoysingh.quicknote.Scarlet -import com.bijoysingh.quicknote.Scarlet.Companion.firebase import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState -import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.maubis.scarlet.base.core.note.MaterialNoteActor import com.maubis.scarlet.base.database.room.note.Note @@ -22,6 +19,9 @@ class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { override fun onlineDelete(context: Context) { super.onlineDelete(context) - gDriveDbState?.notifyRemove(note, notifyChange) + when { + gDrive?.isValid() == true -> gDriveDbState?.notifyRemove(note, notifyChange) + else -> gDriveDbState?.stopTrackingItem(note, notifyChange) + } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 4e818e8b..5b36da2f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -19,6 +19,9 @@ class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { override fun delete() { super.delete() - gDriveDbState?.notifyRemove(tag, notifyChange) + when { + gDrive?.isValid() == true -> gDriveDbState?.notifyRemove(tag, notifyChange) + else -> gDriveDbState?.stopTrackingItem(tag, notifyChange) + } } } \ No newline at end of file From 1b99ef1e9a8c16bfdafad90ea87ee60261522f7a Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 6 Aug 2019 22:56:31 +0100 Subject: [PATCH 064/134] Optimising MainActivity search algorithm and fixing folder count --- .../com/maubis/scarlet/base/MainActivity.kt | 35 ++++++------ .../scarlet/base/support/SearchConfig.kt | 54 ++++++++----------- 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 2e7696f2..3be883bc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -15,6 +15,7 @@ import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note @@ -46,7 +47,7 @@ import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Compani import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_HOME_ENABLED import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount -import com.maubis.scarlet.base.support.SearchConfig +import com.maubis.scarlet.base.support.* import com.maubis.scarlet.base.support.database.HouseKeeperJob import com.maubis.scarlet.base.support.database.Migrator import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -54,9 +55,6 @@ import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.unifiedFolderSearchSynchronous -import com.maubis.scarlet.base.support.unifiedSearchSynchronous import com.maubis.scarlet.base.support.utils.shouldShowWhatsNewSheet import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.search_toolbar_main.* @@ -271,20 +269,25 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { private suspend fun unifiedSearchSynchronous(): List { val allItems = emptyList().toMutableList() - allItems.addAll(unifiedFolderSearchSynchronous(config) + if (config.folders.isNotEmpty()) { + val allNotes = unifiedSearchSynchronous(config) + allItems.addAll(filterOutFolders(allNotes) + .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } + .map { it.await() }) + return allItems + } + + val allNotes = unifiedSearchWithoutFolder(config) + val directAcceptableFolders = filterDirectlyValidFolders(config) + allItems.addAll(CoreConfig.foldersDb.getAll() .map { GlobalScope.async(Dispatchers.IO) { - var notesCount = -1 - if (config.hasFilter()) { - val folderConfig = config.copy() - folderConfig.folders.clear() - folderConfig.folders.add(it) - notesCount = unifiedSearchSynchronous(folderConfig).size - if (notesCount == 0) { - return@async null - } - folderConfig.folders.clear() + val isDirectFolder = directAcceptableFolders.contains(it) + val notesCount = filterFolder(allNotes, it).size + if (config.hasFilter() && notesCount == 0 && !isDirectFolder) { + return@async null } + FolderRecyclerItem( context = this@MainActivity, folder = it, @@ -303,7 +306,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { } .map { it.await() } .filterNotNull()) - allItems.addAll(unifiedSearchSynchronous(config) + allItems.addAll(filterOutFolders(allNotes) .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } .map { it.await() }) return allItems diff --git a/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt b/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt index 1442fe86..cf5412b2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.support +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteState @@ -9,6 +10,7 @@ import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.main.HomeNavigationState import com.maubis.scarlet.base.note.getFullText +import com.maubis.scarlet.base.note.isNoteLockedButAppUnlocked import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet class SearchConfig( @@ -61,7 +63,7 @@ class SearchConfig( fun unifiedSearchSynchronous(config: SearchConfig): List { val sorting = SortingOptionsBottomSheet.getSortingState() - val notes = filterSearchWithoutFolder(config) + val notes = unifiedSearchWithoutFolder(config) .filter { when (config.folders.isEmpty()) { true -> it.folder.isBlank() @@ -71,52 +73,40 @@ fun unifiedSearchSynchronous(config: SearchConfig): List { return sort(notes, sorting) } -private fun filterSearchWithoutFolder(config: SearchConfig): List { +fun filterFolder(notes: List, folder: Folder): List { + val sorting = SortingOptionsBottomSheet.getSortingState() + val filteredNotes = notes.filter { it.folder == folder.uuid } + return sort(filteredNotes, sorting) +} + +fun filterOutFolders(notes: List): List { + val allFoldersUUIDs = ApplicationBase.instance.foldersDatabase().getAll().map { it.uuid } + val sorting = SortingOptionsBottomSheet.getSortingState() + val filteredNotes = notes.filter { !allFoldersUUIDs.contains(it.folder) } + return sort(filteredNotes, sorting) +} + +fun unifiedSearchWithoutFolder(config: SearchConfig): List { return getNotesForMode(config) .filter { config.colors.isEmpty() || config.colors.contains(it.color) } .filter { note -> config.tags.isEmpty() || config.tags.filter { note.tags !== null && note.tags.contains(it.uuid) }.isNotEmpty() } .filter { when { config.text.isBlank() -> true - it.locked -> false + it.locked && !it.isNoteLockedButAppUnlocked() -> false else -> it.getFullText().contains(config.text, true) } } } -fun unifiedFolderSearchSynchronous(config: SearchConfig): List { +fun filterDirectlyValidFolders(config: SearchConfig): List { if (!config.folders.isEmpty()) { return emptyList() } - if (config.text.isNotBlank() || config.tags.isNotEmpty()) { - val folders = HashSet() - if (config.text.isNotBlank()) { - folders.addAll( - foldersDb.getAll() - .filter { config.colors.isEmpty() || config.colors.contains(it.color) } - .filter { it.title.contains(config.text, true) }) - } - folders.addAll( - filterSearchWithoutFolder(config) - .filter { it.folder.isNotBlank() } - .map { it.folder } - .distinct() - .map { foldersDb.getByUUID(it) } - .filterNotNull()) - return folders.toList() - } + return foldersDb.getAll() - .filter { - config.colors.isEmpty() - || config.colors.contains(it.color) - || notesDb.getNotesByFolder(it.uuid).filter { config.colors.contains(it.color) }.isNotEmpty() - } - .filter { - when { - config.text.isBlank() -> true - else -> it.title.contains(config.text, true) - } - } + .filter { config.colors.isEmpty() || config.colors.contains(it.color) } + .filter { it.title.contains(config.text, true) } } fun getNotesForMode(config: SearchConfig): List { From 924d7196e093b23ceceab3c0db12a7fbc6694e45 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 8 Aug 2019 22:20:04 +0100 Subject: [PATCH 065/134] Handling invalid / irrecoverable states from happening --- app/build.gradle | 4 +- .../main/specs/MainActivityBottomBarSpec.kt | 4 +- .../scarlet/base/note/NoteExtensions.kt | 4 -- .../res/drawable/pending_sync_capsule.xml | 9 +++++ build.gradle | 4 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 15 ++++++-- .../drive/GDriveRemoteDatabaseState.kt | 37 ++++++++++--------- .../quicknote/drive/GDriveServiceHelper.kt | 7 ++-- 8 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 base/src/main/res/drawable/pending_sync_capsule.xml diff --git a/app/build.gradle b/app/build.gradle index 3c6e43d9..fa321ce5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 141 - versionName '7.0.11' + versionCode 142 + versionName '7.0.12' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 861ecf19..cd50cc6b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -217,7 +217,7 @@ object MainActivitySyncingNowSpec { .alignContent(YogaAlign.CENTER) .paddingDip(YogaEdge.VERTICAL, 8f) .paddingDip(YogaEdge.HORIZONTAL, 12f) - .backgroundRes(R.drawable.secondary_rounded_bg) + .backgroundRes(R.drawable.pending_sync_capsule) .clickHandler(MainActivitySyncingNow.onClickEvent(context)) .longClickHandler(MainActivitySyncingNow.onLongClickEvent(context)) .child(syncIcon) @@ -225,7 +225,7 @@ object MainActivitySyncingNowSpec { .typeface(FONT_MONSERRAT) .textRes(syncText) .textSizeRes(R.dimen.font_size_normal) - .textColor(colorConfig.toolbarIconColor))) + .textColorRes(R.color.light_secondary_text))) return row.build() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 183910d9..bc61cebf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -55,10 +55,6 @@ fun Note.getFullTextForDirectMarkdownRender(): String { fun Note.getMarkdownForListView(): CharSequence { var text = getFullTextForDirectMarkdownRender() - if (sInternalShowUUID) { - text = "`$uuid`\n\n$text" - } - if (sMarkdownEnabledHome) { return markdownFormatForList(text) } diff --git a/base/src/main/res/drawable/pending_sync_capsule.xml b/base/src/main/res/drawable/pending_sync_capsule.xml new file mode 100644 index 00000000..3e84cd03 --- /dev/null +++ b/base/src/main/res/drawable/pending_sync_capsule.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/build.gradle b/build.gradle index e3b6527d..f3839488 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 141 - ext.appconfig_version = '7.0.11' + ext.appconfig_version_code = 142 + ext.appconfig_version = '7.0.12' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index acf3cb21..4b2c0d87 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -399,13 +399,14 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { * Core Data Functions */ + @Suppress("IMPLICIT_CAST_TO_ANY") private fun insert(type: GDriveDataType, data: GDriveUploadData) { if (!isValidController) { return } val logInfo = "insert(${type.name}, ${data.uuid})" - when (type) { + val localItem = when (type) { GDriveDataType.NOTE -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { notesSync?.insert(data.uuid, this) @@ -427,11 +428,19 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } GDriveDataType.IMAGE -> { - toImageUUID(data.uuid)?.apply { - imageSync?.insert(this) + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null && noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid).exists()) { + imageSync?.insert(imageUUID) + imageUUID + } else { + null } } } + + if (localItem === null) { + gDriveDbState.localDatabaseRemove(type, data.uuid, {}) + } } private fun remove(type: GDriveDataType, data: GDriveUploadData) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt index 2943839f..c1908100 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt @@ -113,25 +113,12 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { * in case the user logs out and the data is deleted */ fun stopTrackingItem(data: Any, onExecution: () -> Unit) { - val removeFromDatabase = { itemType: GDriveDataType, itemUUID: String -> - GlobalScope.launch { - val database = gDriveDatabase - if (database !== null) { - val existing = database.getByUUID(itemType.name, itemUUID) - if (existing !== null) { - database.delete(existing) - } - } - onExecution() - } - } - when { - data is Tag -> removeFromDatabase(GDriveDataType.TAG, data.uuid) - data is Folder -> removeFromDatabase(GDriveDataType.FOLDER, data.uuid) + data is Tag -> localDatabaseRemove(GDriveDataType.TAG, data.uuid, onExecution) + data is Folder -> localDatabaseRemove(GDriveDataType.FOLDER, data.uuid, onExecution) data is Note -> { - removeFromDatabase(GDriveDataType.NOTE, data.uuid) - removeFromDatabase(GDriveDataType.NOTE_META, data.uuid) + localDatabaseRemove(GDriveDataType.NOTE, data.uuid, onExecution) + localDatabaseRemove(GDriveDataType.NOTE_META, data.uuid, onExecution) } else -> maybeThrow("removeFromDb called with unhandled data type") } @@ -210,6 +197,22 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { } } + fun localDatabaseRemove( + itemType: GDriveDataType, + itemUUID: String, + onExecution: () -> Unit) { + GlobalScope.launch { + val database = gDriveDatabase + if (database !== null) { + val existing = database.getByUUID(itemType.name, itemUUID) + if (existing !== null) { + database.delete(existing) + } + } + onExecution() + } + } + private fun notifyImageIds(note: Note, onImageUUID: (ImageUUID) -> Unit) { val imageIds = note.getFormats() .filter { it.formatType == FormatType.IMAGE } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 0934bc0d..e423da7b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -253,15 +253,16 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun removeFileOrFolder(fileUid: String): Task { + fun removeFileOrFolder(fileUid: String): Task { log("GDrive", "removeFileOrFolder($fileUid)") - return execute("removeFileOrFolder", Callable { + return execute("removeFileOrFolder", Callable { try { mDriveService.files().delete(fileUid).execute() + true } catch (exception: Exception) { maybeThrow(exception) + false } - null }) } From 652df0baa20ca713e2f50535c481ed3992cbd278 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 9 Aug 2019 22:53:26 +0100 Subject: [PATCH 066/134] Adding WhatsNewBottomSheet --- app/build.gradle | 4 +- .../com/maubis/scarlet/base/MainActivity.kt | 2 +- .../base/main/sheets/WhatsNewBottomSheet.kt | 65 +++++++------------ .../scarlet/base/note/NoteExtensions.kt | 13 +++- .../note/actions/NoteOptionsBottomSheet.kt | 28 ++++---- .../support/sheets/GridOptionBottomSheet.kt | 2 + .../base/support/specs/GridSectionViewSpec.kt | 20 ++++-- base/src/main/res/values/dimens.xml | 1 + base/src/main/res/values/strings.xml | 7 ++ build.gradle | 4 +- 10 files changed, 79 insertions(+), 67 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fa321ce5..f4382892 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 142 - versionName '7.0.12' + versionCode 143 + versionName '7.0.13' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 3be883bc..f9a5f4d0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -271,7 +271,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { val allItems = emptyList().toMutableList() if (config.folders.isNotEmpty()) { val allNotes = unifiedSearchSynchronous(config) - allItems.addAll(filterOutFolders(allNotes) + allItems.addAll(allNotes .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } .map { it.await() }) return allItems diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index eab9b78b..5865ecab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -1,20 +1,28 @@ package com.maubis.scarlet.base.main.sheets import android.app.Dialog -import android.content.Intent -import android.net.Uri +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.text.Layout import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType +import com.facebook.litho.widget.Image import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle -import com.maubis.scarlet.base.support.specs.BottomSheetBar +import com.maubis.scarlet.base.support.specs.* +import com.maubis.scarlet.base.support.ui.LithoCircleDrawable import com.maubis.scarlet.base.support.ui.ThemeColorType class WhatsNewBottomSheet : LithoBottomSheet() { @@ -31,58 +39,35 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(WHATS_NEW_DETAILS_SUBTITLE) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_xlarge) - .marginDip(YogaEdge.BOTTOM, 4f) - .text(WHATS_NEW_DETAILS_NEW_FEATURES_TITLE) .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .text(Markdown.render(WHATS_NEW_DETAILS_NEW_FEATURES_MD, true)) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_xlarge) - .marginDip(YogaEdge.BOTTOM, 4f) - .text(WHATS_NEW_DETAILS_COMING_SOON_TITLE) - .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .text(Markdown.render(WHATS_NEW_DETAILS_COMING_SOON_MD, true)) .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .child(GridSectionView.create(componentContext) + .maxLines(3) + .numColumns(2) + .iconSizeRes(R.dimen.ultra_large_round_icon_size) + .section(GridSectionItem(options = listOf( + GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), + GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_photo_sync, {}), + GridSectionOptionItem(R.drawable.ic_action_lock, R.string.whats_new_sheet_app_lock, {}), + GridSectionOptionItem(R.drawable.ic_action_select, R.string.whats_new_sheet_selection, {}), + GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_widget, {}), + GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_more_languages, {}) + ))) + .showSeparator(false)) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.import_export_layout_exporting_done) .onPrimaryClick { dismiss() } - .onSecondaryClick { - val url = GOOGLE_TRANSLATE_URL + "en/" + Uri.encode(WHATS_NEW_DETAILS_NEW_FEATURES_MD) - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) - dismiss() - } - .secondaryActionRes(R.string.whats_new_translate) .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } companion object { - val WHATS_NEW_UID = 10 - val GOOGLE_TRANSLATE_URL = "https://translate.google.com/#auto/" + val WHATS_NEW_UID = 11 val WHATS_NEW_DETAILS_SUBTITLE = "A lot has changed in this update, here is a summary of those changes." val WHATS_NEW_DETAILS_NEW_FEATURES_TITLE = "New Features" - val WHATS_NEW_DETAILS_COMING_SOON_TITLE = "Coming Soon" - val WHATS_NEW_DETAILS_NEW_FEATURES_MD = - "- **Checked Items:** You can now enable / disable checked items from moving down when checked.\n\n" + - "- **Bug Fixes:** This release fixes multiple crash issues throughout the application.\n\n" + - "Even more little things which help you enjoy using this app everyday" - val WHATS_NEW_DETAILS_COMING_SOON_MD = - "- **Google Drive Based Sync:** We are building a secure and private Google Drive based sync\n\n" + - "- **Photo Sync:** Drive sync will also allow syncing photos between devices as well.\n\n" } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index bc61cebf..14febbf1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.note +import android.app.Activity import android.content.Context import android.content.Intent import com.github.bijoysingh.starter.util.DateFormatter @@ -28,6 +29,9 @@ import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled import com.maubis.scarlet.base.support.ui.ThemedActivity import java.util.* import kotlin.collections.ArrayList +import android.support.v4.app.ActivityOptionsCompat + + fun Note.log(): String { val log = HashMap() @@ -254,10 +258,15 @@ fun Note.edit(context: Context) { openEdit(context) } -fun Note.view(context: Context) { +fun Note.view(context: Context, options: ActivityOptionsCompat? = null) { val intent = Intent(context, ViewAdvancedNoteActivity::class.java) intent.putExtra(INTENT_KEY_NOTE_ID, this.uid) - context.startActivity(intent) + + when { + (options === null) -> context.startActivity(intent) + else -> context.startActivity(intent, options.toBundle()) + } + } fun Note.viewDistractionFree(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 5d63a1c2..dbd69a1f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -261,12 +261,16 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { } )) options.add(OptionsItem( - title = if (note.pinned) R.string.unpin_note else R.string.pin_note, - subtitle = if (note.pinned) R.string.unpin_note else R.string.pin_note, - icon = R.drawable.ic_pin, + title = if (note.folder.isBlank()) R.string.folder_option_add_to_notebook else R.string.folder_option_change_notebook, + subtitle = R.string.folder_option_add_to_notebook, + icon = R.drawable.ic_folder, listener = View.OnClickListener { - note.pinned = !note.pinned - activity.updateNote(note) + com.maubis.scarlet.base.support.sheets.openSheet(activity, FolderChooserBottomSheet().apply { + this.note = note + this.dismissListener = { + activity.notifyResetOrDismiss() + } + }) dismiss() } )) @@ -308,16 +312,12 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { val options = ArrayList() options.add(OptionsItem( - title = if (note.folder.isBlank()) R.string.folder_option_add_to_notebook else R.string.folder_option_change_notebook, - subtitle = R.string.folder_option_add_to_notebook, - icon = R.drawable.ic_folder, + title = if (note.pinned) R.string.unpin_note else R.string.pin_note, + subtitle = if (note.pinned) R.string.unpin_note else R.string.pin_note, + icon = R.drawable.ic_pin, listener = View.OnClickListener { - com.maubis.scarlet.base.support.sheets.openSheet(activity, FolderChooserBottomSheet().apply { - this.note = note - this.dismissListener = { - activity.notifyResetOrDismiss() - } - }) + note.pinned = !note.pinned + activity.updateNote(note) dismiss() } )) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt index df98445e..fe097964 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt @@ -5,6 +5,7 @@ import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R import com.maubis.scarlet.base.support.specs.GridSectionItem import com.maubis.scarlet.base.support.specs.GridSectionView @@ -28,6 +29,7 @@ abstract class GridOptionBottomSheet : LithoBottomSheet() { GridSectionView.create(componentContext) .marginDip(YogaEdge.HORIZONTAL, 12f) .marginDip(YogaEdge.VERTICAL, 8f) + .iconSizeRes(R.dimen.primary_round_icon_size) .showSeparator(index != options.size) .section(it)) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt index 1a549912..d36a5970 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -34,6 +34,8 @@ object GridOptionSpec { @Prop solidSectionColor: Boolean, @Prop(resType = ResType.COLOR) labelColor: Int, @Prop(resType = ResType.COLOR) iconColor: Int, + @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, + @Prop(resType = ResType.COLOR) maxLines: Int, @Prop(resType = ResType.COLOR) sectionColor: Int): Component { return Column.create(context) .alignItems(YogaAlign.CENTER) @@ -45,7 +47,7 @@ object GridOptionSpec { .bgColor(sectionColor) .iconColor(iconColor) .iconRes(option.icon) - .iconSizeRes(R.dimen.primary_round_icon_size) + .iconSizePx(iconSize) .iconPaddingRes(R.dimen.primary_round_icon_padding) .iconMarginVerticalRes(R.dimen.toolbar_round_icon_margin_vertical) .iconMarginHorizontalRes(R.dimen.toolbar_round_icon_margin_horizontal) @@ -59,8 +61,8 @@ object GridOptionSpec { .textSizeRes(R.dimen.font_size_small) .paddingDip(YogaEdge.VERTICAL, 8f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .minLines(2) - .maxLines(2) + .minLines(maxLines) + .maxLines(maxLines) .ellipsize(TextUtils.TruncateAt.END) .textColor(labelColor)) .clickHandler(GridOption.onClick(context)) @@ -79,7 +81,10 @@ object GridSectionViewSpec { fun onCreate( context: ComponentContext, @Prop section: GridSectionItem, - @Prop showSeparator: Boolean): Component { + @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, + @Prop(optional = true) numColumns: Int?, + @Prop(optional = true) maxLines: Int?, + @Prop(optional = true) showSeparator: Boolean?): Component { val column = Column.create(context) val primaryColor = instance.themeController().get(ThemeColorType.SECONDARY_TEXT) @@ -106,6 +111,8 @@ object GridSectionViewSpec { .flexBasisDip(1f) .solidSectionColor(section.sectionColor != 0) .labelColor(primaryColor) + .maxLines(maxLines ?: 2) + .iconSizePx(iconSize) .iconColor(if (section.sectionColor == 0) primaryColor else Color.WHITE) .sectionColor(if (section.sectionColor == 0) primaryColor else section.sectionColor) .option(visibleOptions[index]) @@ -113,6 +120,7 @@ object GridSectionViewSpec { } } + val numberOfColumns = numColumns ?: 3 var index = 0 while (true) { val row = Row.create(context) @@ -121,14 +129,14 @@ object GridSectionViewSpec { break } - for (delta in 0..2) { + for (delta in 0..(numberOfColumns - 1)) { row.child(getComponentAtIndex(index)) index += 1 } column.child(row) } - if (showSeparator) { + if (showSeparator == true) { column.child(SolidColor.create(context) .color(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) .heightDip(1.5f) diff --git a/base/src/main/res/values/dimens.xml b/base/src/main/res/values/dimens.xml index a69e0dac..ad1d1dd0 100644 --- a/base/src/main/res/values/dimens.xml +++ b/base/src/main/res/values/dimens.xml @@ -18,6 +18,7 @@ 6dp 36dp + 64dp 48dp 12dp diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 40475775..df7be886 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -220,6 +220,13 @@ Know what is new in the recent updates of the app Translate + Sign In with Drive. More Privacy. + Now Photos are Synced too. + App Lock and Single Unlock. + More Languages Supported. + More Widget Settings. + Easier Note Selection. + Copy Block Text Actions Gallery diff --git a/build.gradle b/build.gradle index f3839488..e6c6e028 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 142 - ext.appconfig_version = '7.0.12' + ext.appconfig_version_code = 143 + ext.appconfig_version = '7.0.13' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From bf18810ee6c022926d36629a43cd0a342327342f Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 9 Aug 2019 23:23:13 +0100 Subject: [PATCH 067/134] Fixing bug with App Theme change not propogating --- app/build.gradle | 4 +- .../com/maubis/scarlet/base/MainActivity.kt | 1 + .../scarlet/base/MainActivityExtensions.kt | 56 +++++++++++++++++++ .../sheet/ThemeColorPickerBottomSheet.kt | 1 - .../sheet/UISettingsOptionsBottomSheet.kt | 12 +--- .../base/support/database/MigrationUtils.kt | 6 +- .../scarlet/base/support/ui/ThemeManager.kt | 11 ++-- build.gradle | 4 +- 8 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt diff --git a/app/build.gradle b/app/build.gradle index f4382892..1da10636 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 143 - versionName '7.0.13' + versionCode 144 + versionName '7.0.14' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index f9a5f4d0..916274c7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -81,6 +81,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + handleIntent() // Migrate to the newer version of the tags Migrator(this).start() diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt new file mode 100644 index 00000000..68c85400 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -0,0 +1,56 @@ +package com.maubis.scarlet.base + +import android.content.Context +import android.content.Intent +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.settings.sheet.ThemeColorPickerBottomSheet +import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.ui.sAppTheme + +const val INTENT_KEY_ADDITIONAL_ACTION = "additional_action" + +enum class MainActivityActions { + COLOR_PICKER; + + fun intent(context: Context): Intent { + val intent = Intent(context, MainActivity::class.java) + intent.putExtra(INTENT_KEY_ADDITIONAL_ACTION, this.name) + return intent + } +} + +fun MainActivity.handleIntent() { + val actionFromIntent = intent.getStringExtra(INTENT_KEY_ADDITIONAL_ACTION) + if (actionFromIntent === null || actionFromIntent.isEmpty()) { + return + } + + val action = try { + MainActivityActions.valueOf(actionFromIntent) + } catch (exception: Exception) { + null + } + + if (action === null) { + return + } + performAction(action) +} + +fun MainActivity.performAction(action: MainActivityActions) { + val activity = this + when (action) { + MainActivityActions.COLOR_PICKER -> { + openSheet(this, ThemeColorPickerBottomSheet().apply { + this.onThemeChange = { theme -> + if (sAppTheme != theme.name) { + sAppTheme = theme.name + ApplicationBase.instance.themeController().notifyChange(activity) + activity.startActivity(MainActivityActions.COLOR_PICKER.intent(activity)) + activity.finish() + } + } + }) + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index b8a2e1e0..3b503afc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -103,7 +103,6 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { .isSelected(theme.name == getThemeFromStore().name) .onThemeSelected { newTheme -> onThemeChange(newTheme) - reset(componentContext.androidContext, dialog) } .flexGrow(1f)) } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index 3228a131..c94699df 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -3,15 +3,16 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet +import com.maubis.scarlet.base.performAction import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet.Companion.getSortingState import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet.Companion.getSortingTechniqueLabel import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.ui.KEY_APP_THEME import com.maubis.scarlet.base.support.utils.Flavor class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { @@ -26,14 +27,7 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.home_option_theme_color_subtitle, icon = if (ApplicationBase.instance.themeController().isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, listener = { - com.maubis.scarlet.base.support.sheets.openSheet(activity, ThemeColorPickerBottomSheet().apply { - this.onThemeChange = { theme -> - ApplicationBase.instance.store().put(KEY_APP_THEME, theme.name) - ApplicationBase.instance.themeController().notifyChange(activity) - activity.notifyThemeChange() - reset(activity, dialog) - } - }) + activity.performAction(MainActivityActions.COLOR_PICKER) } )) val isTablet = resources.getBoolean(R.bool.is_tablet) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 11ec9add..d8df3f1f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -10,9 +10,9 @@ import com.maubis.scarlet.base.core.note.getReminder import com.maubis.scarlet.base.note.reminders.ReminderJob import com.maubis.scarlet.base.note.saveWithoutSync import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.KEY_LIST_VIEW -import com.maubis.scarlet.base.support.ui.KEY_APP_THEME import com.maubis.scarlet.base.support.ui.KEY_NIGHT_THEME import com.maubis.scarlet.base.support.ui.Theme +import com.maubis.scarlet.base.support.ui.sAppTheme import com.maubis.scarlet.base.support.utils.getLastUsedAppVersionCode import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope @@ -31,7 +31,7 @@ class Migrator(val context: Context) { fun start() { runTask(KEY_MIGRATE_THEME) { val isNightMode = ApplicationBase.instance.store().get(KEY_NIGHT_THEME, true) - ApplicationBase.instance.store().put(KEY_APP_THEME, if (isNightMode) Theme.DARK.name else Theme.LIGHT.name) + sAppTheme = if (isNightMode) Theme.DARK.name else Theme.LIGHT.name ApplicationBase.instance.themeController().notifyChange(context) } runTask(key = KEY_MIGRATE_REMINDERS) { @@ -63,7 +63,7 @@ class Migrator(val context: Context) { runTaskIf( getLastUsedAppVersionCode() == 0, KEY_MIGRATE_DEFAULT_VALUES) { - ApplicationBase.instance.store().put(KEY_APP_THEME, Theme.DARK.name) + sAppTheme = Theme.DARK.name ApplicationBase.instance.store().put(KEY_LIST_VIEW, true) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index 27f0e7ef..d8a3c18a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -11,6 +11,10 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.utils.throwOrReturn const val KEY_APP_THEME = "KEY_APP_THEME" +var sAppTheme: String + get() = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) + set(value) = ApplicationBase.instance.store().put(KEY_APP_THEME, value) + // Old Theme Key, remove in future once theme is properly handled const val KEY_NIGHT_THEME: String = "KEY_NIGHT_THEME" @@ -95,11 +99,10 @@ class ThemeManager() : IThemeManager { } fun getThemeFromStore(): Theme { - val theme = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) - try { - return Theme.valueOf(theme) + return try { + Theme.valueOf(sAppTheme) } catch (exception: Exception) { - return throwOrReturn(exception, Theme.DARK) + throwOrReturn(exception, Theme.DARK) } } } diff --git a/build.gradle b/build.gradle index e6c6e028..e1b747d4 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 143 - ext.appconfig_version = '7.0.13' + ext.appconfig_version_code = 144 + ext.appconfig_version = '7.0.14' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From e15ed3fbc160f0bbe360bb5ccd7f9d56405b1267 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 11 Aug 2019 22:40:59 +0100 Subject: [PATCH 068/134] Fixing Widget Intents to have Stacks --- app/build.gradle | 4 +-- .../main/activity/WidgetConfigureActivity.kt | 3 +-- .../creation/activity/ViewNoteActivity.kt | 8 ++++++ .../base/widget/AllNotesWidgetProvider.kt | 9 +++---- .../base/widget/CreateNoteWidgetProvider.kt | 26 ++++++++++++------- build.gradle | 4 +-- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1da10636..c8a45593 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 144 - versionName '7.0.14' + versionCode 145 + versionName '7.0.15' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt index 66fd897f..7a1020c9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt @@ -81,8 +81,7 @@ class WidgetConfigureActivity : SelectableNotesActivityBase(), INoteSelectorActi return } - val intent = ViewAdvancedNoteActivity.getIntent(context, note) - val pendingIntent = PendingIntent.getActivity(context, 5000 + note.uid, intent, 0) + val pendingIntent = ViewAdvancedNoteActivity.getIntentWithStack(context, note) val views = RemoteViews(context.getPackageName(), R.layout.widget_layout) views.setTextViewText(R.id.description, getWidgetNoteText(note)) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 1a2e6894..b0047e58 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -1,5 +1,6 @@ package com.maubis.scarlet.base.note.creation.activity +import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Color @@ -39,6 +40,7 @@ import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.ui.* import com.maubis.scarlet.base.support.ui.ColorUtil.darkerColor import com.maubis.scarlet.base.support.utils.bind +import com.maubis.scarlet.base.widget.getPendingIntentWithStack import kotlinx.android.synthetic.main.activity_advanced_note.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -363,6 +365,12 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit intent.putExtra(INTENT_KEY_NOTE_ID, note.uid) return intent } + + fun getIntentWithStack(context: Context, note: Note): PendingIntent? { + val intent = Intent(context, ViewAdvancedNoteActivity::class.java) + intent.putExtra(INTENT_KEY_NOTE_ID, note.uid) + return getPendingIntentWithStack(context, 5000 + note.uid, intent) + } } /** diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt index 72b3941a..94259917 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt @@ -18,9 +18,6 @@ import com.maubis.scarlet.base.support.ui.visibility import com.maubis.scarlet.base.widget.sheet.sWidgetBackgroundColor import com.maubis.scarlet.base.widget.sheet.sWidgetShowToolbar - -const val STORE_KEY_ALL_NOTE_WIDGET = "all_note_widget" - class AllNotesWidgetProvider : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { @@ -42,15 +39,15 @@ class AllNotesWidgetProvider : AppWidgetProvider() { val noteIntent = Intent(context, ViewAdvancedNoteActivity::class.java) noteIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]) - val notePendingIntent = PendingIntent.getActivity(context, 0, noteIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val notePendingIntent = getPendingIntentWithStack(context, 0, noteIntent) views.setPendingIntentTemplate(R.id.list, notePendingIntent) val createNoteIntent = Intent(context, CreateNoteActivity::class.java) - val createNotePendingIntent = PendingIntent.getActivity(context, 23214, createNoteIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val createNotePendingIntent = getPendingIntentWithStack(context, 23214, createNoteIntent) views.setOnClickPendingIntent(R.id.add_note, createNotePendingIntent) val createListNoteIntent = Intent(context, CreateListNoteActivity::class.java) - val createListNotePendingIntent = PendingIntent.getActivity(context, 13123, createListNoteIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val createListNotePendingIntent = getPendingIntentWithStack(context, 13123, createListNoteIntent) views.setOnClickPendingIntent(R.id.add_list, createListNotePendingIntent) val mainIntent = Intent(context, MainActivity::class.java) diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt index d6439e8b..5bfeeade 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt @@ -5,6 +5,7 @@ import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.support.v4.app.TaskStackBuilder import android.widget.RemoteViews import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R @@ -19,24 +20,29 @@ class CreateNoteWidgetProvider : AppWidgetProvider() { for (i in 0 until N) { val appWidgetId = appWidgetIds[i] - val intent = Intent(context, CreateNoteActivity::class.java) - val pendingIntent = PendingIntent.getActivity(context, 23100, intent, 0) - - val views = RemoteViews(context.packageName, R.layout.add_note_widget_layout) - views.setOnClickPendingIntent(R.id.add_note, pendingIntent) + views.setOnClickPendingIntent(R.id.add_note, getPendingIntent(context, CreateNoteActivity::class.java, 23100)) - val intentList = Intent(context, CreateListNoteActivity::class.java) - val pendingIntentList = PendingIntent.getActivity(context, 23101, intentList, 0) + val pendingIntentList = getPendingIntent(context, CreateListNoteActivity::class.java, 23101) views.setOnClickPendingIntent(R.id.add_list, pendingIntentList) - val intentApp = Intent(context, MainActivity::class.java) val pendingIntentApp = PendingIntent.getActivity(context, 23102, intentApp, 0) views.setOnClickPendingIntent(R.id.open_app, pendingIntentApp) - appWidgetManager.updateAppWidget(appWidgetId, views); - + appWidgetManager.updateAppWidget(appWidgetId, views) } } + + private fun getPendingIntent(context: Context, activityClass: Class, requestCode: Int): PendingIntent { + return getPendingIntentWithStack(context, requestCode, Intent(context, activityClass)) + } +} + +fun getPendingIntentWithStack(context: Context, requestCode: Int, resultIntent: Intent, flags: Int = PendingIntent.FLAG_UPDATE_CURRENT): PendingIntent { + return TaskStackBuilder.create(context) + .addNextIntentWithParentStack(Intent(context, MainActivity::class.java)) + .addNextIntent(resultIntent) + .getPendingIntent(requestCode, flags) + ?: PendingIntent.getActivity(context, requestCode, resultIntent, 0) } \ No newline at end of file diff --git a/build.gradle b/build.gradle index e1b747d4..378d7b88 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 144 - ext.appconfig_version = '7.0.14' + ext.appconfig_version_code = 145 + ext.appconfig_version = '7.0.15' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From de823dd0123472afcf0543ac9ed0972b6a8fe8e1 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 11 Aug 2019 22:49:17 +0100 Subject: [PATCH 069/134] Fixing WhatsNew Sheet for OpenSource --- .../base/main/sheets/WhatsNewBottomSheet.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 5865ecab..fcafe1e9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -17,6 +17,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -24,10 +25,19 @@ import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.* import com.maubis.scarlet.base.support.ui.LithoCircleDrawable import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.Flavor class WhatsNewBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + val options = listOf( + if (instance.appFlavor() == Flavor.NONE) null else GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), + if (instance.appFlavor() == Flavor.NONE) null else GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_photo_sync, {}), + GridSectionOptionItem(R.drawable.ic_action_lock, R.string.whats_new_sheet_app_lock, {}), + GridSectionOptionItem(R.drawable.ic_action_select, R.string.whats_new_sheet_selection, {}), + GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_widget, {}), + GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_more_languages, {})) + val component = Column.create(componentContext) .widthPercent(100f) .paddingDip(YogaEdge.VERTICAL, 8f) @@ -45,14 +55,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .maxLines(3) .numColumns(2) .iconSizeRes(R.dimen.ultra_large_round_icon_size) - .section(GridSectionItem(options = listOf( - GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), - GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_photo_sync, {}), - GridSectionOptionItem(R.drawable.ic_action_lock, R.string.whats_new_sheet_app_lock, {}), - GridSectionOptionItem(R.drawable.ic_action_select, R.string.whats_new_sheet_selection, {}), - GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_widget, {}), - GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_more_languages, {}) - ))) + .section(GridSectionItem(options = options.filterNotNull())) .showSeparator(false)) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.import_export_layout_exporting_done) From 93518bd2097ad9785a4ca180b9094909c4fd3786 Mon Sep 17 00:00:00 2001 From: Nicolas Pilli Date: Fri, 16 Aug 2019 00:14:41 +0200 Subject: [PATCH 070/134] Make unchecked elements go up --- .../com/maubis/scarlet/base/core/format/Format.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt index c9f18f7f..a4037d22 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt @@ -80,5 +80,16 @@ fun sectionPreservingSort(formats: List): List { } index += 1 } + while (index > 0) { + val currentItem = mutableFormats[index] + val nextItem = mutableFormats[index - 1] + + if (currentItem.formatType == FormatType.CHECKLIST_UNCHECKED + && nextItem.formatType == FormatType.CHECKLIST_CHECKED) { + Collections.swap(mutableFormats, index, index - 1) + continue + } + index -= 1 + } return mutableFormats } \ No newline at end of file From fa77d3348ab4e5007e8c6f62817bc4515cbe9474 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Tue, 20 Aug 2019 20:49:36 +0100 Subject: [PATCH 071/134] Updating the Pro sheet --- app/build.gradle | 4 +-- .../sheets/InstallProUpsellBottomSheet.kt | 29 +++++++++++------ .../sheet/SecurityOptionsBottomSheet.kt | 31 ++++++++++++++++--- .../widget/sheet/WidgetOptionsBottomSheet.kt | 15 +++++++-- base/src/main/res/values/strings.xml | 7 +++++ build.gradle | 4 +-- 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c8a45593..e089673c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 145 - versionName '7.0.15' + versionCode 146 + versionName '7.0.16' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index 3d3f4986..a3ae91e8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -13,12 +13,21 @@ import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar +import com.maubis.scarlet.base.support.specs.GridSectionItem +import com.maubis.scarlet.base.support.specs.GridSectionOptionItem +import com.maubis.scarlet.base.support.specs.GridSectionView import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity class InstallProUpsellBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { - val activity = context as ThemedActivity + val options = listOf( + GridSectionOptionItem(R.drawable.ic_whats_new, R.string.install_pro_sheet_latest_updates, {}), + GridSectionOptionItem(R.drawable.ic_action_lock, R.string.install_pro_sheet_app_lock, {}), + GridSectionOptionItem(R.drawable.ic_action_day_mode, R.string.install_pro_sheet_app_themes, {}), + GridSectionOptionItem(R.drawable.ic_title_white_48dp, R.string.install_pro_sheet_font_size, {}), + GridSectionOptionItem(R.drawable.ic_action_color, R.string.install_pro_sheet_viewer_bg, {}), + GridSectionOptionItem(R.drawable.icon_widget, R.string.install_pro_sheet_widget_options, {})) + val component = Column.create(componentContext) .widthPercent(100f) .paddingDip(YogaEdge.VERTICAL, 8f) @@ -28,21 +37,23 @@ class InstallProUpsellBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 4f) + .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.why_install_pro) .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .textRes(R.string.why_install_pro_details) .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .child(GridSectionView.create(componentContext) + .maxLines(3) + .numColumns(2) + .iconSizeRes(R.dimen.primary_round_icon_size) + .section(GridSectionItem(options = options)) + .showSeparator(false)) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.install_pro_app) .onPrimaryClick { IntentUtils.openAppPlayStore(activity, "com.bijoysingh.quicknote.pro") dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + } + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index 639b3694..fb9248c3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -5,12 +5,16 @@ import com.facebook.litho.ComponentContext import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.sheets.openCreateSheet import com.maubis.scarlet.base.security.sheets.openVerifySheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.Flavor const val KEY_SECURITY_CODE = "KEY_SECURITY_CODE" const val KEY_FINGERPRINT_ENABLED = "KEY_FINGERPRINT_ENABLED" @@ -50,11 +54,17 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { selected = isPinCodeEnabled() )) + val isLite = instance.appFlavor() == Flavor.LITE options.add(LithoOptionsItem( title = R.string.security_option_lock_app, subtitle = R.string.security_option_lock_app_details, icon = R.drawable.ic_apps_white_48dp, listener = { + if (isLite && !sSecurityAppLockEnabled) { + openSheet(activity, InstallProUpsellBottomSheet()) + return@LithoOptionsItem + } + when { isPinCodeEnabled() -> openVerifySheet( activity = activity, @@ -66,8 +76,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { else -> openCreatePasswordDialog(dialog) } }, - isSelectable = true, - selected = sSecurityAppLockEnabled + actionIcon = when { + sSecurityAppLockEnabled -> R.drawable.ic_done_white_48dp + isLite -> R.drawable.ic_rating + else -> 0 + } )) options.add(LithoOptionsItem( @@ -75,6 +88,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { subtitle = R.string.security_option_ask_pin_always_details, icon = R.drawable.ic_action_grid, listener = { + if (isLite) { + openSheet(activity, InstallProUpsellBottomSheet()) + return@LithoOptionsItem + } + when { isPinCodeEnabled() -> openVerifySheet( activity = activity, @@ -86,8 +104,13 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { else -> openCreatePasswordDialog(dialog) } }, - isSelectable = true, - selected = sSecurityAskPinAlways + isSelectable = !isLite, + selected = sSecurityAskPinAlways, + actionIcon = when { + isLite -> R.drawable.ic_rating + sSecurityAskPinAlways -> R.drawable.ic_done_white_48dp + else -> 0 + } )) val hasFingerprint = Reprint.hasFingerprintRegistered() diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index 4231734f..e696e042 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -11,10 +11,12 @@ import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.note.getFullTextForDirectMarkdownRender import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController @@ -22,6 +24,7 @@ import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.utils.Flavor import com.maubis.scarlet.base.widget.AllNotesWidgetProvider import com.maubis.scarlet.base.widget.NoteWidgetProvider import kotlinx.coroutines.GlobalScope @@ -48,7 +51,7 @@ var sWidgetShowDeletedNotes: Boolean get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, false) set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, value) -const val STORE_KEY_WIDGET_BACKGROUND_COLOR = "widget_background_color" +const val STORE_KEY_WIDGET_BACKGROUND_COLOR = "widget_background_color_v1" var sWidgetBackgroundColor: Int get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_BACKGROUND_COLOR, 0x65000000) set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_BACKGROUND_COLOR, value) @@ -150,11 +153,18 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { isSelectable = true, selected = sWidgetShowToolbar )) + + val isLite = instance.appFlavor() == Flavor.LITE options.add(LithoOptionsItem( title = R.string.widget_option_background_color, subtitle = R.string.widget_option_background_color_details, icon = R.drawable.ic_action_color, listener = { + if (isLite) { + openSheet(activity, InstallProUpsellBottomSheet()) + return@LithoOptionsItem + } + openSheet(activity, ColorPickerBottomSheet().apply { config = ColorPickerDefaultController( title = R.string.widget_option_background_color, @@ -166,7 +176,8 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { }, columns = 6) }) - } + }, + actionIcon = if (!isLite) 0 else R.drawable.ic_rating )) return options } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index df7be886..542e20f5 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -227,6 +227,13 @@ More Widget Settings. Easier Note Selection. + Faster Updates and Requests. + App Lock and Single Unlock. + More App Themes. + Note Font Size Selection. + Viewer Background Color. + More Widget Options. + Copy Block Text Actions Gallery diff --git a/build.gradle b/build.gradle index 378d7b88..b96db5a4 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 145 - ext.appconfig_version = '7.0.15' + ext.appconfig_version_code = 146 + ext.appconfig_version = '7.0.16' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From 448c5d7838239ba1883fb9458ece6a714751b5dd Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 22 Aug 2019 21:50:18 +0100 Subject: [PATCH 072/134] Fixing crash on Note Dialog rotation --- .../scarlet/base/support/sheets/GridBottomSheetBase.kt | 3 ++- .../scarlet/base/support/ui/ThemedBottomSheetFragment.kt | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt index 3322ee5b..2a5ecfd2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt @@ -47,7 +47,8 @@ abstract class GridBottomSheetBase : ThemedBottomSheetFragment() { } fun setOptions(layoutGrid: GridLayout, options: List) { - layoutGrid.columnCount = if (resources.getBoolean(R.bool.is_tablet)) 4 else 3 + val context = layoutGrid.context + layoutGrid.columnCount = if (context.resources.getBoolean(R.bool.is_tablet)) 4 else 3 for (option in options) { if (!option.visible) { continue diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt index 9d49afe8..d2b4725e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt @@ -16,6 +16,8 @@ import com.maubis.scarlet.base.config.ApplicationBase abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { + var appContext: Context? = null + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val isTablet = maybeContext()?.resources?.getBoolean(R.bool.is_tablet) ?: false val dialog = when { @@ -30,14 +32,15 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { if (dialog == null) { return } + appContext = dialog.context.applicationContext resetBackground(dialog) } fun themedActivity(): Activity = activity ?: context as AppCompatActivity - fun themedContext(): Context = context ?: activity!! + fun themedContext(): Context = maybeContext()!! - fun maybeContext(): Context? = context ?: activity + fun maybeContext(): Context? = context ?: activity ?: appContext abstract fun getBackgroundView(): Int From 4f9304cfb0da52e483da3996a8e41e0c575dc922 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 22 Aug 2019 22:15:20 +0100 Subject: [PATCH 073/134] #89 Color Sorting --- .../base/core/note/NoteSortingUtils.kt | 25 +++++++++++++++++-- .../sheet/SortingOptionsBottomSheet.kt | 12 ++++++++- base/src/main/res/values/strings.xml | 1 + 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt index fd932642..e882bbf4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt @@ -8,6 +8,20 @@ enum class SortingTechnique() { NEWEST_FIRST, OLDEST_FIRST, ALPHABETICAL, + NOTE_COLOR, +} + +/** + * Helper class which allow comparison of a pair of objects + */ +class ComparablePair, U : Comparable>(val first: T, val second: U) : Comparable> { + override fun compareTo(other: ComparablePair): Int { + val firstComparison = first.compareTo(other.first) + return when { + firstComparison == 0 -> second.compareTo(other.second) + else -> firstComparison + } + } } fun sort(notes: List, sortingTechnique: SortingTechnique): List { @@ -25,8 +39,15 @@ fun sort(notes: List, sortingTechnique: SortingTechnique): List { val content = note.getFullText().trim().filter { ((it in 'a'..'z') || (it in 'A'..'Z')) } - if (note.pinned || content.isBlank()) 0 - else content[0].toUpperCase().toInt() + + val sortValue = when { + (note.pinned || content.isBlank()) -> 0 + else -> content[0].toUpperCase().toInt() + } + ComparablePair(sortValue, note.updateTimestamp) + } + SortingTechnique.NOTE_COLOR -> notes.sortedBy { note -> + ComparablePair(note.color, note.updateTimestamp) } else -> notes.sortedByDescending { note -> if (note.pinned) Long.MAX_VALUE diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt index c2887f38..47f41f4f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt @@ -54,6 +54,15 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { }, selected = sorting == SortingTechnique.ALPHABETICAL )) + options.add(LithoChooseOptionsItem( + title = getSortingTechniqueLabel(SortingTechnique.NOTE_COLOR), + listener = { + setSortingState(SortingTechnique.NOTE_COLOR) + listener() + reset(componentContext.androidContext, dialog) + }, + selected = sorting == SortingTechnique.NOTE_COLOR + )) return options } @@ -62,7 +71,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { const val KEY_SORTING_TECHNIQUE = "KEY_SORTING_TECHNIQUE" fun getSortingState(): SortingTechnique { - return SortingTechnique.values()[ApplicationBase.instance.store().get(KEY_SORTING_TECHNIQUE, SortingTechnique.NEWEST_FIRST.ordinal)] + return SortingTechnique.values()[ApplicationBase.instance.store().get(KEY_SORTING_TECHNIQUE, SortingTechnique.LAST_MODIFIED.ordinal)] } fun getSortingTechniqueLabel(technique: SortingTechnique): Int { @@ -71,6 +80,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { SortingTechnique.NEWEST_FIRST -> R.string.sort_sheet_newest_first SortingTechnique.OLDEST_FIRST -> R.string.sort_sheet_oldest_first SortingTechnique.ALPHABETICAL -> R.string.sort_sheet_alphabetical + SortingTechnique.NOTE_COLOR -> R.string.sort_sheet_note_color } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 542e20f5..ed0632ee 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -204,6 +204,7 @@ Sort Notes By Newest First + Note Color Oldest First Most Recently Modified Alphabetical From 3aeab282ec6367fe5a1ab564d2705830c1017ee2 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 22 Aug 2019 22:32:35 +0100 Subject: [PATCH 074/134] #89 Adding Sorting by Tags --- .../base/core/note/NoteSortingUtils.kt | 15 +++++ .../sheet/SortingOptionsBottomSheet.kt | 59 ++++--------------- base/src/main/res/values/strings.xml | 1 + 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt index e882bbf4..94a79180 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt @@ -9,6 +9,7 @@ enum class SortingTechnique() { OLDEST_FIRST, ALPHABETICAL, NOTE_COLOR, + NOTE_TAGS, } /** @@ -49,6 +50,20 @@ fun sort(notes: List, sortingTechnique: SortingTechnique): List { SortingTechnique.NOTE_COLOR -> notes.sortedBy { note -> ComparablePair(note.color, note.updateTimestamp) } + SortingTechnique.NOTE_TAGS -> { + val tagCounterMap = HashMap() + notes.map { it.getTagUUIDs() }.forEach { tags -> + tags.forEach { tag -> + tagCounterMap[tag] = (tagCounterMap[tag] ?: 0) + 1 + } + } + notes.sortedByDescending { + val noteTagScore = it.getTagUUIDs().sumBy { tag -> + tagCounterMap[tag] ?: 0 + } + ComparablePair(ComparablePair(noteTagScore, it.tags), it.updateTimestamp) + } + } else -> notes.sortedByDescending { note -> if (note.pinned) Long.MAX_VALUE else note.timestamp diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt index 47f41f4f..0b9c4d32 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt @@ -12,57 +12,23 @@ import com.maubis.scarlet.base.support.sheets.LithoChooseOptionsItem class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { var listener: () -> Unit = {} - override fun title(): Int = R.string.sort_sheet_title + override fun title(): Int = R.string.sort_sheet_title override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val sorting = getSortingState() val options = ArrayList() - options.add(LithoChooseOptionsItem( - title = getSortingTechniqueLabel(SortingTechnique.LAST_MODIFIED), - listener = { - setSortingState(SortingTechnique.LAST_MODIFIED) - listener() - reset(componentContext.androidContext, dialog) - }, - selected = sorting == SortingTechnique.LAST_MODIFIED - )) - options.add(LithoChooseOptionsItem( - title = getSortingTechniqueLabel(SortingTechnique.NEWEST_FIRST), - listener = { - setSortingState(SortingTechnique.NEWEST_FIRST) - listener() - reset(componentContext.androidContext, dialog) - }, - selected = sorting == SortingTechnique.NEWEST_FIRST - )) - options.add(LithoChooseOptionsItem( - title = getSortingTechniqueLabel(SortingTechnique.OLDEST_FIRST), - listener = { - setSortingState(SortingTechnique.OLDEST_FIRST) - listener() - reset(componentContext.androidContext, dialog) - }, - selected = sorting == SortingTechnique.OLDEST_FIRST - )) - options.add(LithoChooseOptionsItem( - title = getSortingTechniqueLabel(SortingTechnique.ALPHABETICAL), - listener = { - setSortingState(SortingTechnique.ALPHABETICAL) - listener() - reset(componentContext.androidContext, dialog) - }, - selected = sorting == SortingTechnique.ALPHABETICAL - )) - options.add(LithoChooseOptionsItem( - title = getSortingTechniqueLabel(SortingTechnique.NOTE_COLOR), - listener = { - setSortingState(SortingTechnique.NOTE_COLOR) - listener() - reset(componentContext.androidContext, dialog) - }, - selected = sorting == SortingTechnique.NOTE_COLOR - )) + SortingTechnique.values().forEach { technique -> + options.add(LithoChooseOptionsItem( + title = getSortingTechniqueLabel(technique), + listener = { + setSortingState(technique) + listener() + reset(componentContext.androidContext, dialog) + }, + selected = sorting == technique + )) + } return options } @@ -81,6 +47,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { SortingTechnique.OLDEST_FIRST -> R.string.sort_sheet_oldest_first SortingTechnique.ALPHABETICAL -> R.string.sort_sheet_alphabetical SortingTechnique.NOTE_COLOR -> R.string.sort_sheet_note_color + SortingTechnique.NOTE_TAGS -> R.string.sort_sheet_note_tags } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index ed0632ee..8d16ba6c 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -205,6 +205,7 @@ Sort Notes By Newest First Note Color + Note Tags Oldest First Most Recently Modified Alphabetical From 3b1023e0c3b58370d7baa3d32e6dcaa5c8f0be52 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 24 Aug 2019 00:10:38 +0100 Subject: [PATCH 075/134] Create FUNDING.yml --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..3e334d38 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: bijoyskochar +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: bijoyskochar +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 51182f9d813803cb147778a2c139f2d5eca3fd8f Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 24 Aug 2019 01:20:10 +0100 Subject: [PATCH 076/134] Update FUNDING.yml --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3e334d38..58f2723a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username +patreon: bijoyskochar open_collective: # Replace with a single Open Collective username ko_fi: bijoyskochar tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel From a071337519d25174062eba49f64fb10637227c46 Mon Sep 17 00:00:00 2001 From: Fs00 Date: Wed, 21 Aug 2019 20:41:19 +0200 Subject: [PATCH 077/134] Fix minimum text holder height being based on hint text --- .../scarlet/base/core/format/FormatType.kt | 2 +- .../formats/recycler/FormatTextViewHolder.kt | 44 +++++++++++++++---- base/src/main/res/layout/item_format_code.xml | 2 - .../main/res/layout/item_format_heading.xml | 2 - base/src/main/res/layout/item_format_list.xml | 4 +- .../src/main/res/layout/item_format_quote.xml | 2 - base/src/main/res/layout/item_format_tag.xml | 2 - base/src/main/res/layout/item_format_text.xml | 2 - 8 files changed, 38 insertions(+), 22 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt index af39f361..680b2150 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt @@ -13,5 +13,5 @@ enum class FormatType { CODE, QUOTE, SEPARATOR, - EMPTY, + EMPTY } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt index 48f6809c..57d16ae3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt @@ -30,7 +30,7 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder protected val edit: EditText = root.findViewById(R.id.edit) protected val actionMove: ImageView = root.findViewById(R.id.action_move_icon) - protected var format: Format? = null + protected lateinit var format: Format init { edit.addTextChangedListener(this) @@ -66,7 +66,7 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder edit.setBackgroundColor(config.backgroundColor) edit.visibility = visibility(config.editable) edit.isEnabled = config.editable - + showHintWhenTextIsEmpty() when { config.editable -> edit.setText(data.text) @@ -87,22 +87,33 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder } } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (format === null || !edit.isFocused) { + override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) { + if (!edit.isFocused) { return } - format!!.text = s.toString() - activity.setFormat(format!!) + + format.text = text.toString() + activity.setFormat(format) + showHintWhenTextIsEmpty() + } + + // Workaround to avoid this holder being higher than the text contained in it when hint text + // occupies more lines than the text inserted by the user + private fun showHintWhenTextIsEmpty() { + edit.hint = when { + format.text.isEmpty() -> format.getHint() + else -> "" + } } override fun afterTextChanged(text: Editable) { text.clearMarkdownSpans() if (sEditorLiveMarkdown) { - text.setFormats(Markdown.getSpanInfo(format!!.text).spans) + text.setFormats(Markdown.getSpanInfo(format.text).spans) } } @@ -136,4 +147,21 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder maybeThrow(context as AppCompatActivity, exception) } } + + private fun Format.getHint(): String { + return when (formatType) { + FormatType.TEXT, FormatType.TAG -> context.getString(R.string.format_hint_text) + FormatType.HEADING, + FormatType.SUB_HEADING, + FormatType.HEADING_3 + -> context.getString(R.string.format_hint_heading) + FormatType.NUMBERED_LIST, + FormatType.CHECKLIST_UNCHECKED, + FormatType.CHECKLIST_CHECKED + -> context.getString(R.string.format_hint_list) + FormatType.CODE -> context.getString(R.string.format_hint_code) + FormatType.QUOTE -> context.getString(R.string.format_hint_quote) + else -> "" + } + } } diff --git a/base/src/main/res/layout/item_format_code.xml b/base/src/main/res/layout/item_format_code.xml index d178b6f1..6add9912 100644 --- a/base/src/main/res/layout/item_format_code.xml +++ b/base/src/main/res/layout/item_format_code.xml @@ -7,7 +7,6 @@ \ No newline at end of file diff --git a/base/src/main/res/layout/item_format_list.xml b/base/src/main/res/layout/item_format_list.xml index b56983e3..8b4aaa90 100644 --- a/base/src/main/res/layout/item_format_list.xml +++ b/base/src/main/res/layout/item_format_list.xml @@ -17,11 +17,9 @@ android:id="@+id/text" style="@style/FormatEdit" android:autoLink="web" - android:hint="@string/format_hint_list" android:linksClickable="true" /> + style="@style/FormatText" /> \ No newline at end of file diff --git a/base/src/main/res/layout/item_format_quote.xml b/base/src/main/res/layout/item_format_quote.xml index df54c4f0..60575e67 100644 --- a/base/src/main/res/layout/item_format_quote.xml +++ b/base/src/main/res/layout/item_format_quote.xml @@ -17,7 +17,6 @@ android:id="@+id/text" style="@style/FormatText" android:autoLink="web" - android:hint="@string/format_hint_quote" android:linksClickable="true" android:paddingStart="@dimen/spacing_xxsmall" android:paddingBottom="@dimen/spacing_small" @@ -26,7 +25,6 @@ \ No newline at end of file diff --git a/base/src/main/res/layout/item_format_tag.xml b/base/src/main/res/layout/item_format_tag.xml index 02add132..464cd86c 100644 --- a/base/src/main/res/layout/item_format_tag.xml +++ b/base/src/main/res/layout/item_format_tag.xml @@ -9,13 +9,11 @@ android:id="@+id/text" style="@style/FormatText" android:autoLink="web" - android:hint="@string/format_hint_text" android:linksClickable="true" /> \ No newline at end of file diff --git a/base/src/main/res/layout/item_format_text.xml b/base/src/main/res/layout/item_format_text.xml index 3a8f666f..adc0db0e 100644 --- a/base/src/main/res/layout/item_format_text.xml +++ b/base/src/main/res/layout/item_format_text.xml @@ -9,12 +9,10 @@ android:id="@+id/text" style="@style/FormatText" android:autoLink="web" - android:hint="@string/format_hint_text" android:linksClickable="true" /> \ No newline at end of file From 562b29c5463779f5966e2f781bc8f752a810d3ac Mon Sep 17 00:00:00 2001 From: Fs00 Date: Fri, 30 Aug 2019 20:31:12 +0200 Subject: [PATCH 078/134] Add Italian translation changes from #65 --- base/src/main/res/values-it/strings.xml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/base/src/main/res/values-it/strings.xml b/base/src/main/res/values-it/strings.xml index 191c73a5..5b4c9086 100644 --- a/base/src/main/res/values-it/strings.xml +++ b/base/src/main/res/values-it/strings.xml @@ -186,12 +186,12 @@ Supporta lo sviluppatore per i massivi costi del server, per eseguire la sincronizzazione del cloud \n\n Prima le funzionalità più recenti \n\n Alcune funzioni aggiuntive saranno disponibili solo per gli utenti Pro - copia + Copia Blocca azioni di testo Galleria - telecamera - Parla ad alta voce - Parlare a voce alta + Fotocamera + Leggi ad alta voce + Leggi ad alta voce L\'eliminazione di questo elemento eliminerà l\'immagine da tutti i dispositivi. Immagine non su questo dispositivo L\'immagine non può essere caricata @@ -206,12 +206,11 @@ Solo una volta Quotidiano Promemoria e allarmi - %s è open source e chiunque è aperto a contribuire a migliorarlo. Attualmente è costruito e gestito da %s. + %s è open source e chiunque è libero di contribuire per migliorarla. Attualmente è sviluppata e gestita da %s. %s è una semplice app per prendere appunti. Permette di inserire rapidamente Rich Text senza rendere l\'esperienza molto difficile da usare. Rende il multi-tasking un gioco da ragazzi. - Ciao, siamo una coppia di designer e programmatori, che ha creato %s. - Ci sforziamo di creare app pubblicitarie gratuite, pubblicitarie o minime, progettate con cura, che siano di grande utilità per tutti! - %d linee sulla schermata iniziale - Distrazione gratuita + Ciao, siamo una coppia di designer e programmatori che ha creato %s. Ci sforziamo di creare app progettate con cura, senza o con poca pubblicità, che siano di grande utilità per tutti! + %d linee sulla schermata principale + Senza distrazioni Tema dell\'app From 437326ef3e0fe5f690e57d692e32f353e0d1e085 Mon Sep 17 00:00:00 2001 From: Fs00 Date: Sat, 31 Aug 2019 17:28:34 +0200 Subject: [PATCH 079/134] Fix and update Italian translation --- base/src/main/res/values-it/strings.xml | 236 +++++++++++++----------- base/src/main/res/values/strings.xml | 56 +++--- 2 files changed, 160 insertions(+), 132 deletions(-) diff --git a/base/src/main/res/values-it/strings.xml b/base/src/main/res/values-it/strings.xml index 5b4c9086..56563c98 100644 --- a/base/src/main/res/values-it/strings.xml +++ b/base/src/main/res/values-it/strings.xml @@ -7,7 +7,7 @@ Nessuna nota Sembra che non hai aggiunto alcuna nota. Fai clic per aggiungere una nota. Modifica nota - Apri in popup + Apri in pop-up Copia nota Invia nota Scegli un\'azione… @@ -16,8 +16,8 @@ Aggiungi testo citato… Aggiungi elemento… Aggiungi codice… - Markdown Support - Consenti la formattazione markdown + Supporto Markdown + Consenti la formattazione Markdown Opzioni e impostazioni Esporta note Esporta le note nella memoria del dispositivo @@ -27,14 +27,14 @@ Scopri di più sull\'app e sugli sviluppatori Valuta e recensisci Facci sapere quanto ti è piaciuta l\'app - Dai i permessi - L\'importazione e l\'esportazione richiedono il permesso di archiviazione. Si prega di dare il permesso quando richiesto. - Permetti + Concedi permessi + L\'importazione e l\'esportazione richiedono il permesso di archiviazione. Si prega di concedere il permesso quando richiesto. + Concedi Importa backup da file Importa file Esportato nel file Esportazione fallita - Esportazione nel file… + Esportazione su file… Ok Condividi Vota su Play Store @@ -45,14 +45,14 @@ Proteggi nota Sblocca nota Sicurezza - Opzioni di sicurezza e di blocco + Blocco delle note e opzioni di sicurezza Opzioni di sicurezza Codice di accesso Imposta un PIN di 4 cifre per proteggere le note - Sblocca con Fingerprint + Sblocca con l\'impronta Le note verranno sbloccate con l\'impronta digitale Impronta digitale disabilitata - Le note non utilizzeranno un\'impronta digitale + Le note non potranno essere sbloccate con l\'impronta digitale Inserisci il nuovo PIN Inserisci il PIN per sbloccare Inserisci il PIN corrente @@ -91,26 +91,26 @@ Archivio Cestino Crea notifica - Note in notifica + Note nell\'area di notifica Protetti Aggiungi nuovo tag… Modifica tag… - inserisci il tag + Inserisci il tag Crea un nuovo tag Scegli un tag… Importa - Markdown in Elenco note + Markdown nell\'elenco delle note Abilita la formattazione Markdown nell\'elenco delle note Cambia tag Ordine note - Progetto Open Source + Progetto open source Scopri di più sul progetto open source Librerie - Progetto Open Source + Progetto open source Data del promemoria Ora del promemoria Frequenza ripetizione @@ -123,25 +123,25 @@ Sblocca nota - Interfaccia ed esperienza - Scegli l\'aspetto dell\'app. - Chi siamo - Scopri di più su di noi e sull\'app. + Interfaccia ed esperienza utente + Modifica l\'aspetto dell\'app + Informazioni sull\'app + Scopri di più su di noi e sull\'app Colore nota predefinita Scegli il colore predefinito delle note - Limite lunghezza note + Lunghezza anteprima note Backup e importazione Backup, importazione ed esportazione delle note Installa dallo store - Installa app da Google Store per Cloud Sync + Installa l\'app dallo store di Google per effettuare la sincronizzazione con il cloud Nessun PIN impostato Non hai impostato il PIN. Vuoi impostarlo ora? - Dopo + Più tardi Non chiedere mai Imposta @@ -153,50 +153,50 @@ Vuoi eliminare definitivamente questa nota? Elimina Annulla - Le note vengono cancellate per sempre dopo 7 giorni + Le note vengono cancellate definitivamente dopo 7 giorni Aggiungi nota - Aggiungi lista di controllo + Aggiungi nota con elenco Seleziona una nota - Seleziona Note + Seleziona note Impostazioni Nota cancellata o protetta Dimensione carattere - Regola le dimensioni del carattere del testo nella pagina delle note. Puoi vedere come sarebbe in questa anteprima. - %d dimensioni del testo nel visualizzatore note + Regola le dimensioni del carattere del testo delle note. Puoi vedere come apparirà in questa anteprima. + Dimensione del testo nel visualizzatore note: %d Cosa c\'è di nuovo - Scopri cosa c\'è di nuovo nei recenti aggiornamenti dell\'app + Scopri cosa c\'è di nuovo negli ultimi aggiornamenti dell\'app Traduci Accedi all\'app - Accedi per il backup e la sincronizzazione del cloud + Accedi per il backup e la sincronizzazione con il cloud Disconnessione - Esci per interrompere il backup del cloud - Accesso Google non riuscito - politica sulla riservatezza - Politica sulla privacy dell\'app per il contenuto - Installa l\'app Pro - Perché installare Pro - Supporta lo sviluppatore per i massivi costi del server, per eseguire la sincronizzazione del cloud \n\n Prima le funzionalità più recenti \n\n Alcune funzioni aggiuntive saranno disponibili solo per gli utenti Pro + Esci per interrompere il backup sul cloud + Accesso a Google non riuscito + Privacy policy + Privacy policy dell\'app per il contenuto + Installa la versione Pro + Perché installare la versione Pro + Supporta lo sviluppatore per i costi del server necessari ad eseguire la sincronizzazione con il cloud \n\n Ottieni prima le ultime funzionalità \n\n Alcune funzioni aggiuntive saranno disponibili solo per gli utenti Pro Copia - Blocca azioni di testo + Azioni blocco di testo Galleria Fotocamera Leggi ad alta voce Leggi ad alta voce - L\'eliminazione di questo elemento eliminerà l\'immagine da tutti i dispositivi. - Immagine non su questo dispositivo + La cancellazione di questo elemento eliminerà l\'immagine da tutti i dispositivi. + Immagine non presente su questo dispositivo L\'immagine non può essere caricata Le immagini non sono sincronizzate - Le immagini non sono sincronizzate su tutti i dispositivi. Le tue immagini non verranno visualizzate su altri dispositivi durante la visualizzazione della nota! + Le immagini non sono sincronizzate su tutti i dispositivi. Le tue immagini non verranno visualizzate sugli altri dispositivi! Capisco Esporta automaticamente Esporta frequentemente le note in un file esterno come backup @@ -205,9 +205,9 @@ Solo una volta Quotidiano - Promemoria e allarmi + Promemoria e sveglie %s è open source e chiunque è libero di contribuire per migliorarla. Attualmente è sviluppata e gestita da %s. - %s è una semplice app per prendere appunti. Permette di inserire rapidamente Rich Text senza rendere l\'esperienza molto difficile da usare. Rende il multi-tasking un gioco da ragazzi. + %s è una semplice app per prendere appunti. Permette di inserire rapidamente testo formattato senza rendere complicata l\'esperienza utente. Rende il multi-tasking un gioco da ragazzi. Ciao, siamo una coppia di designer e programmatori che ha creato %s. Ci sforziamo di creare app progettate con cura, senza o con poca pubblicità, che siano di grande utilità per tutti! %d linee sulla schermata principale Senza distrazioni @@ -215,31 +215,31 @@ Tema dell\'app Seleziona il colore di sfondo per il tema - Seleziona tema app + Seleziona il tema dell\'app Fai clic per aggiungere o modificare i tag - Seleziona Note + Seleziona note Seleziona ed esegui azioni su più note contemporaneamente La nota è stata spostata nel cestino La nota è stata cancellata - Disfare - Disabilita backup - Abilita backup + Annulla + Escludi dal backup + Includi nel backup Notebook vuoto 1 nota - %d Note - Crea nuovo taccuino + %d note + Crea nuovo blocco note Modifica blocco note - Aggiungi notebook + Aggiungi blocco note Aggiungi al blocco note - Modifica notebook + Modifica blocco note Note recenti Informazione Tocca per installare la versione più recente dell\'app - Toccare per aggiornare a Scarlet Pro - Installa l\'app Pro - Installa l\'app Pro per sbloccare funzionalità e supportare lo sviluppatore - Migrazione di Notes a Scarlet Pro - Migra tutte le tue note all\'app pro + Tocca per passare a Scarlet Pro + Installa la versione Pro + Installa la versione Pro per sbloccare funzionalità e supportare lo sviluppatore + Migra le note su Scarlet Pro + Migra tutte le tue note sulla versione Pro Elimina note e altro Elimina note, tag e altri dati nell\'app Elimina tutte le note @@ -250,87 +250,87 @@ Elimina tutti i tag nell\'app Elimina tutto Elimina note, tag e cartelle nell\'app - Note sullo sfondo del visualizzatore + Sfondo visualizzatore note Unisci note - Esporta come markdown - Esporta le note in formato markdown. (Non è possibile importarli nuovamente nell\'app) + Esporta come Markdown + Esporta le note in formato Markdown (non è possibile importarle nuovamente nell\'app) Usa il colore del tema per lo sfondo Usa il colore della nota per lo sfondo - Abilitare - disattivare + Abilita + Disabilita Abilita sincronizzazione cartella - Sincronizzazione cartelle - Sincronizza tutte le note, i tag e le cartelle in una cartella esterna. Questo ti aiuta a sincronizzare utilizzando altre app tra dispositivi, oltre a averne una copia nel caso in cui sia necessario eliminare l\'app. - Esportazione in cartella - Sincronizza tutte le note, i tag, ecc. In una cartella esterna - Esporta note bloccate + Sincronizzazione cartella + Sincronizza tutte le note, i tag e le cartelle in una cartella esterna. Questo ti aiuta a sincronizzare i dati utilizzando altre app su più dispositivi, oltre ad averne una copia nel caso in cui tu decida di eliminare l\'app. + Esportazione nella cartella + Sincronizza tutte le note, i tag, ecc. in una cartella esterna + Esporta note protette Alfabetico Disponibile su Scarlet Pro - Effettua anche il backup delle note che sono bloccate + Effettua anche il backup delle note che sono protette Opzioni dell\'editor - Modifica le impostazioni e l\'utilizzo per l\'editor delle note - Opzioni di markdown come predefinite - Mostra i pulsanti rapidi di markdown sulla barra degli strumenti come impostazione predefinita - Markdown in tempo reale - Scegli se vuoi che il markdown sia visibile come tipo. L\'abilitazione può influire sulle prestazioni su note di grandi dimensioni. - Sposta elementi controllati - Gli elementi selezionati si spostano in fondo all\'elenco. Deseleziona non ripristina la posizione. + Modifica le impostazioni dell\'editor delle note + Mostra opzioni Markdown come predefinite + Mostra i pulsanti rapidi per la formattazione Markdown sulla barra degli strumenti come impostazione predefinita + Formattazione Markdown in tempo reale + La formattazione Markdown verrà applicata mentre scrivi. Può influire sulle prestazioni su note di grandi dimensioni. + Riordina elementi selezionati + Gli elementi selezionati verranno spostati in fondo all\'elenco Mostra maniglie di movimento - Mostra la maniglia per spostare gli elementi verso l\'alto o verso il basso. Puoi comunque spostare le cose toccando l\'angolo. - Esempi di markdown - Impostazioni del widget + Mostra la maniglia per spostare gli elementi. Se l\'opzione è disabilitata, potrai comunque spostarli toccando l\'angolo. + Esempi di formattazione Markdown + Impostazioni dei widget Modifica le impostazioni per i widget della schermata principale Abilita formattazione Le note vengono visualizzate con formattazione nei widget della schermata principale - Mostra note bloccate - Consenti alle note bloccate di essere visualizzate nei widget della schermata principale + Mostra note protette + Consenti alle note protette di essere visualizzate nei widget della schermata principale Mostra note archiviate Consenti alle note archiviate di essere visualizzate nei widget della schermata principale Mostra note nel cestino Consenti alle note nel cestino di essere visualizzate nei widget della schermata principale Aiuto e domande comuni - Trova assistenza su come utilizzare le funzionalità nell\'app + Trova assistenza su come utilizzare le funzionalità dell\'app Sincronizzazione Backup in sospeso Opzioni sviluppatore - Modifica le impostazioni interne nell\'applicazione utilizzata per il debug - Eccezioni registro - Registra le eccezioni rilevate su una nota fissa per consentire l\'inoltro allo sviluppatore - Mostra foglio eccezioni - Mostra le eccezioni rilevate su un foglio, se possibile, per consentire l\'inoltro allo sviluppatore - Aggiungi eccezioni - Lancia e blocca l\'applicazione in caso di eccezioni. Verrà ripristinato dopo 5 arresti anomali - Abilita schermo intero - Rendi l\'app a schermo intero per consentire schermate e registrazioni + Modifica le impostazioni interne utilizzate per il debug + Registra eccezioni + Registra le eccezioni rilevate su una nota fissata per consentirne l\'inoltro allo sviluppatore + Mostra eccezioni + Mostra le eccezioni rilevate, se possibile, per consentirne l\'inoltro allo sviluppatore + Propaga eccezioni + Fa crashare l\'applicazione in caso di eccezioni. L\'opzione verrà ripristinata dopo 5 arresti anomali + Abilita modalità schermo intero + Rendi l\'app a schermo intero per consentire screenshot e registrazioni Mostra UUID note - Mostra gli ID univoci delle note nella vista della schermata principale - Eccezioni false - Lancia un\'eccezione falsa per testare le funzionalità dell\'eccezione + Mostra gli ID univoci delle note nella schermata principale + Eccezioni fasulle + Lancia un\'eccezione fasulla per testare le funzionalità relative alle eccezioni Eccezione generata - App Crash - posta + Fai crashare l\'app + Invia email Connessione fallita Sincronizza su Google Drive - Google Drive ti consente di sincronizzare le tue note tra i dispositivi utilizzando il tuo account Google Drive. - Hai effettuato l\'accesso a Google Firebase prima? + Google Drive ti consente di sincronizzare le tue note tra i dispositivi utilizzando il tuo account Google. + Hai effettuato l\'accesso a Google Firebase in precedenza? Accedi a Google Drive - Accessoâ € ¦ + Accesso in corso… Esci da Scarlet Smetti di sincronizzare note, tag e cartelle. Interrompi la sincronizzazione di note, tag e cartelle su Google Drive. - I tuoi dati sull\'app e su Google Drive saranno ancora lì. + I tuoi dati sull\'app e su Google Drive rimarranno al loro posto. Esci da Drive - Disconnessioneâ € ¦ + Disconnessione in corso… Note, tag e cartelle sono memorizzati sul tuo Google Drive, quindi solo tu puoi controllarne l\'accesso. Le tue foto vengono caricate e sincronizzate anche su tutti i dispositivi. Ripristina dati da Firebase Accedi con Google - Accessoâ € ¦ - Conservavamo le tue informazioni su Google Firebase. Dopo aver effettuato l\'accesso, li ripristineremo sul tuo dispositivo - Le nuove note e modifiche NON verranno sincronizzate con Google Firebase e dovrai accedere a Google Drive - Una volta recuperate le note, è possibile eliminarle da Google Firebase e passare successivamente + Accesso in corso… + Conservavamo i tuoi dati su Google Firebase. Dopo aver effettuato l\'accesso, li ripristineremo sul tuo dispositivo + Le tue note NON verranno più sincronizzate con Google Firebase e dovrai accedere a Google Drive + Una volta recuperate le note, è possibile eliminarle da Google Firebase ed effettuare la migrazione I tuoi dati non vengono sincronizzati! La sincronizzazione dei dati basata sull\'accesso legacy è disabilitata, considera la possibilità di passare alla sincronizzazione basata su Google Drive. Trasferisci dati da Firebase a Drive @@ -341,6 +341,34 @@ Sincronizzazione foto. Prossimi passi Prima elimineremo le tue note da Firebase e ti disconnetteremo. - Puoi quindi accedere a Google Drive e noi cariceremo i tuoi dati insieme alle immagini. + Puoi quindi accedere a Google Drive e noi caricheremo i tuoi dati insieme alle immagini. + Vuoi eliminare definitivamente queste note? + Sblocca l\'app + Inserisci il PIN di 4 cifre per effettuare lo sblocco + Inserisci il PIN di 4 cifre o usa l\'impronta per effettuare lo sblocco + Dimenticami + Disconnettimi e rimuovi tutti i dati memorizzati online + Blocco dell\'app e sblocco singolo delle note. + Più temi. + Selezione della dimensione del carattere. + Aggiornamenti e richieste più veloci. + Più opzioni per i widget. + Colore di sfondo dinamico del visualizzatore note. + Locale + Remoto + Creato + Eliminato + Non disponibile + Aggiornato + Richiedi sempre il PIN + Richiedi il PIN per ciascuna nota protetta, anche se è stato già inserito + Abilita blocco dell\'applicazione + Usa il PIN per sbloccare l\'applicazione + Colore delle note + Tag delle note + Sfondo del widget + Scegli il colore di sfondo per il widget multi-nota + Mostra barra delle azioni + Mostra la barra delle azioni nel widget multi-nota diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 8d16ba6c..c491b131 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -115,10 +115,10 @@ Security Options Pass Code Set a 4 digit PIN to lock notes - Enable App Lock - Use PIN to unlock the whole application - Always ask for PIN - Enter PIN for all notes, even if previously entered + Enable App Lock + Use PIN to unlock the whole application + Always ask for PIN + Enter PIN for all notes, even if previously entered Unlock with Fingerprint The notes will unlock with the fingerprint Fingerprint disabled @@ -204,8 +204,8 @@ Sort Notes By Newest First - Note Color - Note Tags + Note Color + Note Tags Oldest First Most Recently Modified Alphabetical @@ -229,12 +229,12 @@ More Widget Settings. Easier Note Selection. - Faster Updates and Requests. - App Lock and Single Unlock. - More App Themes. - Note Font Size Selection. - Viewer Background Color. - More Widget Options. + Faster Updates and Requests. + App Lock and Single Unlock. + More App Themes. + Note Font Size Selection. + Viewer Background Color. + More Widget Options. Copy Block Text Actions @@ -274,7 +274,7 @@ Are you sure? Would you like to permanently delete the notes in the trash folder? Would you like to permanently delete this note? - Would you like to permanently delete these notes? + Would you like to permanently delete these notes? Delete Cancel Notes are deleted forever after 7 days @@ -305,8 +305,8 @@ - Forget Me - Logout and remove all online data + Forget Me + Logout and remove all online data Scarlet @@ -378,11 +378,11 @@ Show Notes in Trash Allow notes in trash to be shown in home screen widgets - Widget Background - Choose the background color for the multi-notes widget + Widget Background + Choose the background color for the multi-notes widget - Show Action Toolbar - Multi-notes widget shows the toolbar + Show Action Toolbar + Multi-notes widget shows the toolbar @@ -392,13 +392,13 @@ Syncing Pending Backup - Local - Remote + Local + Remote - Unavailable - Created - Deleted - Updated + Unavailable + Created + Deleted + Updated Developer Options @@ -468,9 +468,9 @@ - Unlock App - Enter 4 digit PIN or use fingerprint to unlock - Enter 4 digit PIN to unlock + Unlock App + Enter 4 digit PIN or use fingerprint to unlock + Enter 4 digit PIN to unlock From 5cb6c506a0768108bcbea8c55cf8b8301156718c Mon Sep 17 00:00:00 2001 From: Fs00 Date: Thu, 22 Aug 2019 12:01:29 +0200 Subject: [PATCH 080/134] Fix note creation in current folder when using No notes card --- .../main/java/com/maubis/scarlet/base/MainActivity.kt | 2 +- ...yRecyclerHolder.kt => EmptyFolderRecyclerHolder.kt} | 10 +++++++--- ...EmptyRecyclerItem.kt => EmptyFolderRecyclerItem.kt} | 2 +- .../scarlet/base/note/recycler/NoteAppAdapter.kt | 6 +++--- .../selection/activity/SelectableNotesActivityBase.kt | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) rename base/src/main/java/com/maubis/scarlet/base/main/recycler/{EmptyRecyclerHolder.kt => EmptyFolderRecyclerHolder.kt} (54%) rename base/src/main/java/com/maubis/scarlet/base/main/recycler/{EmptyRecyclerItem.kt => EmptyFolderRecyclerItem.kt} (83%) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 916274c7..31ad1e26 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -242,7 +242,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { adapter.addItem(GenericRecyclerItem(RecyclerItem.Type.TOOLBAR)) } if (notes.isEmpty()) { - adapter.addItem(EmptyRecyclerItem()) + adapter.addItem(EmptyFolderRecyclerItem()) return } notes.forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerHolder.kt similarity index 54% rename from base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt rename to base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerHolder.kt index 9f43e917..29f0c7bc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerHolder.kt @@ -4,16 +4,20 @@ import android.content.Context import android.os.Bundle import android.view.View import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder -import com.github.bijoysingh.starter.util.IntentUtils +import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem -class EmptyRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder(context, itemView) { +class EmptyFolderRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder(context, itemView) { override fun populate(data: RecyclerItem, extra: Bundle) { setFullSpan() itemView.setOnClickListener { - IntentUtils.startActivity(context, CreateNoteActivity::class.java) + val newNoteIntent = CreateNoteActivity.getNewNoteIntent( + context, + folder = (context as MainActivity).config.folders.firstOrNull()?.uuid ?: "" + ) + context.startActivity(newNoteIntent) } } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerItem.kt similarity index 83% rename from base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerItem.kt rename to base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerItem.kt index a3c61681..44103329 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerItem.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.main.recycler import com.maubis.scarlet.base.support.recycler.RecyclerItem -class EmptyRecyclerItem : RecyclerItem() { +class EmptyFolderRecyclerItem : RecyclerItem() { override val type = RecyclerItem.Type.EMPTY } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt index bcc9116c..131ea4a7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt @@ -5,7 +5,7 @@ import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewAdapter import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewControllerItem import com.maubis.scarlet.base.R import com.maubis.scarlet.base.export.recycler.FileImportViewHolder -import com.maubis.scarlet.base.main.recycler.EmptyRecyclerHolder +import com.maubis.scarlet.base.main.recycler.EmptyFolderRecyclerHolder import com.maubis.scarlet.base.main.recycler.InformationRecyclerHolder import com.maubis.scarlet.base.main.recycler.ToolbarMainRecyclerHolder import com.maubis.scarlet.base.note.folder.FolderRecyclerHolder @@ -37,7 +37,7 @@ fun getRecyclerItemControllerList( list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) - .holderClass(EmptyRecyclerHolder::class.java) + .holderClass(EmptyFolderRecyclerHolder::class.java) .build()) list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.INFORMATION.ordinal) @@ -79,7 +79,7 @@ fun getSelectableRecyclerItemControllerList( list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) - .holderClass(EmptyRecyclerHolder::class.java) + .holderClass(EmptyFolderRecyclerHolder::class.java) .spanSize(2) .build()) return list diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index 0ad0013f..0fe7acb8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -15,7 +15,7 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.main.recycler.EmptyRecyclerItem +import com.maubis.scarlet.base.main.recycler.EmptyFolderRecyclerItem import com.maubis.scarlet.base.note.folder.SelectorFolderRecyclerItem import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.note.recycler.NoteRecyclerItem @@ -70,7 +70,7 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct adapter.clearItems() if (notes.isEmpty()) { - adapter.addItem(EmptyRecyclerItem()) + adapter.addItem(EmptyFolderRecyclerItem()) } var lastFolder = "" From 9d7f98bfe029cac255a1736f86013cacd886b800 Mon Sep 17 00:00:00 2001 From: Fs00 Date: Thu, 22 Aug 2019 12:07:51 +0200 Subject: [PATCH 081/134] Don't display information items when searching --- base/src/main/java/com/maubis/scarlet/base/MainActivity.kt | 4 ++-- ...{EmptyFolderRecyclerHolder.kt => EmptyRecyclerHolder.kt} | 2 +- .../{EmptyFolderRecyclerItem.kt => EmptyRecyclerItem.kt} | 2 +- .../com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt | 6 +++--- .../note/selection/activity/SelectableNotesActivityBase.kt | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) rename base/src/main/java/com/maubis/scarlet/base/main/recycler/{EmptyFolderRecyclerHolder.kt => EmptyRecyclerHolder.kt} (85%) rename base/src/main/java/com/maubis/scarlet/base/main/recycler/{EmptyFolderRecyclerItem.kt => EmptyRecyclerItem.kt} (83%) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 31ad1e26..c0ec7dce 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -240,15 +240,15 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { adapter.clearItems() if (!isInSearchMode) { adapter.addItem(GenericRecyclerItem(RecyclerItem.Type.TOOLBAR)) + addInformationItem(1) } if (notes.isEmpty()) { - adapter.addItem(EmptyFolderRecyclerItem()) + adapter.addItem(EmptyRecyclerItem()) return } notes.forEach { adapter.addItem(it) } - addInformationItem(1) } private fun addInformationItem(index: Int) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt similarity index 85% rename from base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerHolder.kt rename to base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt index 29f0c7bc..b2049092 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt @@ -8,7 +8,7 @@ import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem -class EmptyFolderRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder(context, itemView) { +class EmptyRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder(context, itemView) { override fun populate(data: RecyclerItem, extra: Bundle) { setFullSpan() diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerItem.kt similarity index 83% rename from base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerItem.kt rename to base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerItem.kt index 44103329..a3c61681 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyFolderRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerItem.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.main.recycler import com.maubis.scarlet.base.support.recycler.RecyclerItem -class EmptyFolderRecyclerItem : RecyclerItem() { +class EmptyRecyclerItem : RecyclerItem() { override val type = RecyclerItem.Type.EMPTY } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt index 131ea4a7..bcc9116c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt @@ -5,7 +5,7 @@ import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewAdapter import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewControllerItem import com.maubis.scarlet.base.R import com.maubis.scarlet.base.export.recycler.FileImportViewHolder -import com.maubis.scarlet.base.main.recycler.EmptyFolderRecyclerHolder +import com.maubis.scarlet.base.main.recycler.EmptyRecyclerHolder import com.maubis.scarlet.base.main.recycler.InformationRecyclerHolder import com.maubis.scarlet.base.main.recycler.ToolbarMainRecyclerHolder import com.maubis.scarlet.base.note.folder.FolderRecyclerHolder @@ -37,7 +37,7 @@ fun getRecyclerItemControllerList( list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) - .holderClass(EmptyFolderRecyclerHolder::class.java) + .holderClass(EmptyRecyclerHolder::class.java) .build()) list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.INFORMATION.ordinal) @@ -79,7 +79,7 @@ fun getSelectableRecyclerItemControllerList( list.add(MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) - .holderClass(EmptyFolderRecyclerHolder::class.java) + .holderClass(EmptyRecyclerHolder::class.java) .spanSize(2) .build()) return list diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index 0fe7acb8..0ad0013f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -15,7 +15,7 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.main.recycler.EmptyFolderRecyclerItem +import com.maubis.scarlet.base.main.recycler.EmptyRecyclerItem import com.maubis.scarlet.base.note.folder.SelectorFolderRecyclerItem import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.note.recycler.NoteRecyclerItem @@ -70,7 +70,7 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct adapter.clearItems() if (notes.isEmpty()) { - adapter.addItem(EmptyFolderRecyclerItem()) + adapter.addItem(EmptyRecyclerItem()) } var lastFolder = "" From e2417f2f18f0c34c9e0e65f6f40acc8e932f7207 Mon Sep 17 00:00:00 2001 From: Fs00 Date: Sun, 1 Sep 2019 21:01:42 +0200 Subject: [PATCH 082/134] Fix possible NPE when closing search in main activity --- base/src/main/java/com/maubis/scarlet/base/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index c0ec7dce..9c934709 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -437,10 +437,10 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { fun setSearchMode(mode: Boolean) { isInSearchMode = mode - searchToolbar.visibility = if (isInSearchMode) View.VISIBLE else View.GONE searchBox.setText("") if (isInSearchMode) { + searchToolbar.visibility = View.VISIBLE tryOpeningTheKeyboard() GlobalScope.launch(Dispatchers.Main) { GlobalScope.async(Dispatchers.IO) { tagAndColorPicker.reset() }.await() @@ -449,6 +449,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { searchBox.requestFocus() } else { tryClosingTheKeyboard() + searchToolbar.visibility = View.GONE config.clearSearchBar() setupData() } From 9578bb26c0606a7b229fef0cae302d1968153020 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 30 Sep 2019 22:16:07 +0100 Subject: [PATCH 083/134] [Theme] Adding support for automatic theme --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 90 +--------------- base/src/main/AndroidManifest.xml | 102 +++++++++++++++++- .../com/maubis/scarlet/base/MainActivity.kt | 17 ++- .../scarlet/base/MainActivityExtensions.kt | 2 + .../sheet/ThemeColorPickerBottomSheet.kt | 86 ++++++++++----- .../base/support/ui/IThemeChangeListener.kt | 5 + .../scarlet/base/support/ui/IThemeManager.kt | 4 + .../scarlet/base/support/ui/ThemeManager.kt | 39 ++++++- .../scarlet/base/support/ui/ThemedActivity.kt | 25 ++++- base/src/main/res/values/strings.xml | 5 + build.gradle | 4 +- scarlet/src/main/AndroidManifest.xml | 100 ++--------------- 13 files changed, 268 insertions(+), 215 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeChangeListener.kt diff --git a/app/build.gradle b/app/build.gradle index e089673c..18342315 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 146 - versionName '7.0.16' + versionCode 147 + versionName '7.1.1' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 291b20a6..b6ea897a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -15,20 +16,12 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/AppTheme"> - - - - + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> @@ -40,81 +33,6 @@ android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index a280d8f8..63e9b81c 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -1,12 +1,108 @@ - + + android:windowSoftInputMode="adjustResize" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 916274c7..694e9b5c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -1,6 +1,7 @@ package com.maubis.scarlet.base import android.content.BroadcastReceiver +import android.content.res.Configuration import android.os.Bundle import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView @@ -55,6 +56,8 @@ import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.ToolbarColorConfig import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.ui.sAutomaticTheme +import com.maubis.scarlet.base.support.ui.setThemeFromSystem import com.maubis.scarlet.base.support.utils.shouldShowWhatsNewSheet import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.search_toolbar_main.* @@ -90,15 +93,25 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { setupRecyclerView() setListeners() - notifyThemeChange() + + if (sAutomaticTheme) { + setThemeFromSystem(this) + } + instance.themeController().notifyChange(this) if (shouldShowWhatsNewSheet()) { openSheet(this, WhatsNewBottomSheet()) } } + override fun onConfigurationChanged(configuration: Configuration?) { + super.onConfigurationChanged(configuration) + startActivity(MainActivityActions.NIL.intent(this)) + finish() + } + fun setListeners() { - snackbar = MainSnackbar(bottomSnackbar, { setupData() }) + snackbar = MainSnackbar(bottomSnackbar) { setupData() } deleteTrashIcon.setOnClickListener { openDeleteTrashSheet(this@MainActivity) } searchBackButton.setOnClickListener { onBackPressed() diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt index 68c85400..4992b874 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -10,6 +10,7 @@ import com.maubis.scarlet.base.support.ui.sAppTheme const val INTENT_KEY_ADDITIONAL_ACTION = "additional_action" enum class MainActivityActions { + NIL, COLOR_PICKER; fun intent(context: Context): Intent { @@ -40,6 +41,7 @@ fun MainActivity.handleIntent() { fun MainActivity.performAction(action: MainActivityActions) { val activity = this when (action) { + MainActivityActions.NIL -> {} MainActivityActions.COLOR_PICKER -> { openSheet(this, ThemeColorPickerBottomSheet().apply { this.onThemeChange = { theme -> diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index 3b503afc..1e43cba1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -2,6 +2,8 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog import android.graphics.Color +import android.os.Build +import android.support.v7.app.AppCompatActivity import com.facebook.litho.* import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout @@ -11,16 +13,14 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet -import com.maubis.scarlet.base.support.sheets.LithoBottomSheet -import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle -import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.sheets.* import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.specs.RoundIcon -import com.maubis.scarlet.base.support.ui.Theme +import com.maubis.scarlet.base.support.ui.* import com.maubis.scarlet.base.support.ui.ThemeManager.Companion.getThemeFromStore -import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.Flavor @LayoutSpec @@ -81,32 +81,62 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { .textRes(R.string.theme_page_title) .marginDip(YogaEdge.HORIZONTAL, 0f)) - var flex: Row.Builder? = null - Theme.values().forEachIndexed { index, theme -> - if (index % 4 == 0) { - column.child(flex) - flex = Row.create(componentContext) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.VERTICAL, 12f) - } + if (Build.VERSION.SDK_INT >= 28) { + column.child(OptionItemLayout.create(componentContext) + .option(LithoOptionsItem( + title = R.string.theme_use_system_theme, + subtitle = R.string.theme_use_system_theme_details, + icon = R.drawable.ic_action_color, + listener = {}, + isSelectable = true, + selected = sAutomaticTheme, + actionIcon = if (instance.appFlavor() == Flavor.PRO) 0 else R.drawable.ic_rating + )) + .onClick { + val context = componentContext.androidContext as AppCompatActivity + if (instance.appFlavor() != Flavor.PRO) { + openSheet(context, InstallProUpsellBottomSheet()) + return@onClick + } + + sAutomaticTheme = !sAutomaticTheme + if (sAutomaticTheme) { + setThemeFromSystem(context) + onThemeChange(instance.themeController().get()) + } + reset(context, dialog) + }) + } - val disabled = when { - ApplicationBase.instance.appFlavor() == Flavor.PRO -> false - theme == Theme.DARK || theme == Theme.LIGHT -> false - else -> true + if (!sAutomaticTheme) { + var flex: Row.Builder? = null + Theme.values().forEachIndexed { index, theme -> + if (index % 4 == 0) { + column.child(flex) + flex = Row.create(componentContext) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + } + + val disabled = when { + ApplicationBase.instance.appFlavor() == Flavor.PRO -> false + theme == Theme.DARK || theme == Theme.LIGHT -> false + else -> true + } + flex?.child( + ThemeColorPickerItem.create(componentContext) + .theme(theme) + .isDisabled(disabled) + .isSelected(theme.name == getThemeFromStore().name) + .onThemeSelected { newTheme -> + onThemeChange(newTheme) + } + .flexGrow(1f)) } - flex?.child( - ThemeColorPickerItem.create(componentContext) - .theme(theme) - .isDisabled(disabled) - .isSelected(theme.name == getThemeFromStore().name) - .onThemeSelected { newTheme -> - onThemeChange(newTheme) - } - .flexGrow(1f)) + column.child(flex) } - column.child(flex) + column.child(EmptySpec.create(componentContext).widthPercent(100f).heightDip(24f)) column.child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.import_export_layout_exporting_done) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeChangeListener.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeChangeListener.kt new file mode 100644 index 00000000..02c62701 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeChangeListener.kt @@ -0,0 +1,5 @@ +package com.maubis.scarlet.base.support.ui + +interface IThemeChangeListener { + fun onChange(theme: Theme) +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeManager.kt index b24a0c8f..191c1fd7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/IThemeManager.kt @@ -10,9 +10,13 @@ interface IThemeManager { fun isNightTheme(): Boolean + fun get(): Theme + fun get(type: ThemeColorType): Int fun get(context: Context, theme: Theme, type: ThemeColorType): Int fun get(context: Context, lightColor: Int, darkColor: Int): Int + + fun register(listener: IThemeChangeListener) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index d8a3c18a..e8a84a05 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -1,6 +1,7 @@ package com.maubis.scarlet.base.support.ui import android.content.Context +import android.content.res.Configuration import android.graphics.Color import android.os.Build import android.support.v4.content.ContextCompat @@ -9,25 +10,52 @@ import com.maubis.markdown.MarkdownConfig import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.support.utils.throwOrReturn +import java.lang.ref.WeakReference const val KEY_APP_THEME = "KEY_APP_THEME" var sAppTheme: String get() = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) set(value) = ApplicationBase.instance.store().put(KEY_APP_THEME, value) +const val KEY_AUTOMATIC_THEME = "automatic_theme" +var sAutomaticTheme: Boolean + get() = ApplicationBase.instance.store().get(KEY_AUTOMATIC_THEME, false) + set(value) = ApplicationBase.instance.store().put(KEY_AUTOMATIC_THEME, value) + +fun setThemeFromSystem(context: Context) { + val configuration = context.resources.configuration + val systemBasedTheme = when (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_NO -> Theme.LIGHT.name + Configuration.UI_MODE_NIGHT_YES -> Theme.VERY_DARK.name + else -> Theme.VERY_DARK.name + } + if (systemBasedTheme === sAppTheme) { + return + } + sAppTheme = systemBasedTheme +} // Old Theme Key, remove in future once theme is properly handled const val KEY_NIGHT_THEME: String = "KEY_NIGHT_THEME" -class ThemeManager() : IThemeManager { +class ThemeManager : IThemeManager { lateinit var theme: Theme + var listeners = HashSet>() var map = HashMap() override fun setup(context: Context) { theme = getThemeFromStore() notifyChange(context) } + override fun get(): Theme { + return theme + } + + override fun register(listener: IThemeChangeListener) { + listeners.add(WeakReference(listener)) + } + override fun isNightTheme() = theme.isNightTheme override fun get(type: ThemeColorType): Int = map[type] ?: Color.WHITE @@ -47,10 +75,17 @@ class ThemeManager() : IThemeManager { } if (map[ThemeColorType.TOOLBAR_BACKGROUND] == map[ThemeColorType.BACKGROUND]) { - map[ThemeColorType.TOOLBAR_BACKGROUND] = ColorUtil.darkerOrSlightlyDarkerColor(map[ThemeColorType.TOOLBAR_BACKGROUND] ?: 0) + map[ThemeColorType.TOOLBAR_BACKGROUND] = ColorUtil.darkerOrSlightlyDarkerColor(map[ThemeColorType.TOOLBAR_BACKGROUND] + ?: 0) } setMarkdownConfig(context) + for (reference in listeners) { + val listener = reference.get() + if (listener !== null) { + listener.onChange(theme) + } + } } private fun setMarkdownConfig(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index 95d01670..b97b0640 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -1,19 +1,33 @@ package com.maubis.scarlet.base.support.ui import android.content.Context +import android.content.res.Configuration import android.os.Build +import android.os.Bundle +import android.os.PersistableBundle import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager import com.maubis.scarlet.base.BuildConfig +import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.settings.sheet.sInternalEnableFullScreen import com.maubis.scarlet.base.support.utils.maybeThrow -abstract class ThemedActivity : AppCompatActivity() { +abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { abstract fun notifyThemeChange() + override fun onChange(theme: Theme) { + notifyThemeChange() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + instance.themeController().register(this) + } + fun setSystemTheme(color: Int = getStatusBarColor()) { setStatusBarColor(color) setStatusBarTextColor() @@ -24,6 +38,15 @@ abstract class ThemedActivity : AppCompatActivity() { fullScreenView() } + override fun onConfigurationChanged(configuration: Configuration?) { + super.onConfigurationChanged(configuration) + if (configuration === null || !sAutomaticTheme) { + return + } + setThemeFromSystem(this) + instance.themeController().notifyChange(this) + } + fun fullScreenView() { if (!sInternalEnableFullScreen) { return diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 8d16ba6c..6394a244 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -477,4 +477,9 @@ + + Use System Theme + Use the light or dark theme from the system + + diff --git a/build.gradle b/build.gradle index b96db5a4..8070b3ba 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 146 - ext.appconfig_version = '7.0.16' + ext.appconfig_version_code = 147 + ext.appconfig_version = '7.1.1' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' diff --git a/scarlet/src/main/AndroidManifest.xml b/scarlet/src/main/AndroidManifest.xml index 1a857d1d..599d5f3e 100644 --- a/scarlet/src/main/AndroidManifest.xml +++ b/scarlet/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -16,27 +17,13 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/AppTheme"> - - - - - - + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + android:windowSoftInputMode="stateHidden" + android:configChanges="uiMode"> @@ -48,67 +35,14 @@ android:resource="@xml/shortcuts" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - @@ -116,18 +50,6 @@ - - - - - - - - \ No newline at end of file From 537219736a2985939bcabd5696fb48b8abbfaae2 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Mon, 30 Sep 2019 22:33:42 +0100 Subject: [PATCH 084/134] Enabling all Pro features for Open Source --- .../sheet/BackupSettingsOptionsBottomSheet.kt | 6 ++--- .../main/recycler/InformationRecyclerItem.kt | 14 +++++------ .../base/main/sheets/WhatsNewBottomSheet.kt | 23 ++++++------------- .../note/actions/NoteOptionsBottomSheet.kt | 10 ++++---- .../sheet/AboutSettingsOptionsBottomSheet.kt | 6 ++--- .../sheet/SecurityOptionsBottomSheet.kt | 5 ++-- .../sheet/SettingsOptionsBottomSheet.kt | 12 ++++------ .../sheet/ThemeColorPickerBottomSheet.kt | 14 ++++++----- .../sheet/UISettingsOptionsBottomSheet.kt | 17 ++++++-------- .../utils/{FlavourUtils.kt => FlavorUtils.kt} | 8 ++++++- .../widget/sheet/WidgetOptionsBottomSheet.kt | 5 ++-- 11 files changed, 51 insertions(+), 69 deletions(-) rename base/src/main/java/com/maubis/scarlet/base/support/utils/{FlavourUtils.kt => FlavorUtils.kt} (76%) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt index 38d65ace..bfd44671 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt @@ -5,17 +5,15 @@ import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.sheets.openUnlockSheet -import com.maubis.scarlet.base.settings.sheet.SecurityOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.home_option_backup_options @@ -31,7 +29,7 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { IntentUtils.openAppPlayStore(context) dismiss() }, - visible = ApplicationBase.instance.appFlavor() == Flavor.NONE + visible = FlavorUtils.isOpenSource() )) options.add(LithoOptionsItem( title = R.string.home_option_export, diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt index a3cf4bce..2b2bf8d1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt @@ -13,8 +13,7 @@ import com.maubis.scarlet.base.main.activity.INTENT_KEY_DIRECT_NOTES_TRANSFER import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.utils.Flavor -import com.maubis.scarlet.base.support.utils.FlavourUtils +import com.maubis.scarlet.base.support.utils.FlavorUtils import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -102,7 +101,7 @@ fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { fun shouldShowInstallProInformationItem(): Boolean { return probability(0.01f) && ApplicationBase.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT - && ApplicationBase.instance.appFlavor() != Flavor.PRO + && !FlavorUtils.isPro() } fun getInstallProInformationItem(context: Context): InformationRecyclerItem { @@ -117,8 +116,7 @@ fun getInstallProInformationItem(context: Context): InformationRecyclerItem { } fun shouldShowSignInformationItem(context: Context): Boolean { - if (ApplicationBase.instance.authenticator().isLoggedIn(context) - || ApplicationBase.instance.appFlavor() == Flavor.NONE) { + if (ApplicationBase.instance.authenticator().isLoggedIn(context) || FlavorUtils.isOpenSource()) { return false } if (ApplicationBase.instance.store().get(KEY_FORCE_SHOW_SIGN_IN, false)) { @@ -147,8 +145,8 @@ fun notifyProUpsellShown() { fun shouldShowMigrateToProAppInformationItem(context: Context): Boolean { - return ApplicationBase.instance.appFlavor() == Flavor.LITE - && FlavourUtils.hasProAppInstalled(context) + return FlavorUtils.isLite() + && FlavorUtils.hasProAppInstalled(context) && !ApplicationBase.instance.store().get(KEY_MIGRATE_TO_PRO_SUCCESS, false) } @@ -163,7 +161,7 @@ fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem val intent = Intent(Intent.ACTION_SEND) .putExtra(INTENT_KEY_DIRECT_NOTES_TRANSFER, notes.await()) .setType("text/plain") - .setPackage(FlavourUtils.PRO_APP_PACKAGE_NAME) + .setPackage(FlavorUtils.PRO_APP_PACKAGE_NAME) try { context.startActivity(intent) ApplicationBase.instance.store().put(KEY_MIGRATE_TO_PRO_SUCCESS, true) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index fcafe1e9..70e36d7a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -1,38 +1,29 @@ package com.maubis.scarlet.base.main.sheets import android.app.Dialog -import android.graphics.Color -import android.graphics.drawable.Drawable -import android.text.Layout import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext -import com.facebook.litho.annotations.LayoutSpec -import com.facebook.litho.annotations.OnCreateLayout -import com.facebook.litho.annotations.Prop -import com.facebook.litho.annotations.ResType -import com.facebook.litho.widget.Image import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge -import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance -import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle -import com.maubis.scarlet.base.support.specs.* -import com.maubis.scarlet.base.support.ui.LithoCircleDrawable +import com.maubis.scarlet.base.support.specs.BottomSheetBar +import com.maubis.scarlet.base.support.specs.GridSectionItem +import com.maubis.scarlet.base.support.specs.GridSectionOptionItem +import com.maubis.scarlet.base.support.specs.GridSectionView import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils class WhatsNewBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val options = listOf( - if (instance.appFlavor() == Flavor.NONE) null else GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), - if (instance.appFlavor() == Flavor.NONE) null else GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_photo_sync, {}), + if (FlavorUtils.isOpenSource()) null else GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), + if (FlavorUtils.isOpenSource()) null else GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_photo_sync, {}), GridSectionOptionItem(R.drawable.ic_action_lock, R.string.whats_new_sheet_app_lock, {}), GridSectionOptionItem(R.drawable.ic_action_select, R.string.whats_new_sheet_selection, {}), GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_widget, {}), diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index dbd69a1f..3edfd8e5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -4,7 +4,6 @@ import android.app.Dialog import android.content.Intent import android.support.v4.content.ContextCompat import android.view.View -import android.view.ViewGroup import android.widget.GridLayout import android.widget.LinearLayout import android.widget.LinearLayout.VERTICAL @@ -35,7 +34,7 @@ import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async @@ -382,13 +381,12 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.view_distraction_free, icon = R.drawable.ic_action_distraction_free, listener = View.OnClickListener { - if (ApplicationBase.instance.appFlavor() == Flavor.PRO) { + if (!FlavorUtils.isLite()) { note.viewDistractionFree(activity) return@OnClickListener } com.maubis.scarlet.base.support.sheets.openSheet(activity, InstallProUpsellBottomSheet()) }, - visible = ApplicationBase.instance.appFlavor() != Flavor.NONE, invalid = activity.lockedContentIsHidden() && note.locked )) options.add(OptionsItem( @@ -410,7 +408,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { activity.updateNote(note) dismiss() }, - visible = note.disableBackup && ApplicationBase.instance.appFlavor() != Flavor.NONE, + visible = note.disableBackup && FlavorUtils.isPlayStore(), invalid = activity.lockedContentIsHidden() && note.locked )) options.add(OptionsItem( @@ -422,7 +420,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { activity.updateNote(note) dismiss() }, - visible = !note.disableBackup && ApplicationBase.instance.appFlavor() != Flavor.NONE, + visible = !note.disableBackup && FlavorUtils.isPlayStore(), invalid = activity.lockedContentIsHidden() && note.locked )) return options diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt index 0fb19f68..5de0a078 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt @@ -4,15 +4,13 @@ import android.app.Dialog import android.content.Intent import android.net.Uri import com.facebook.litho.ComponentContext -import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils import com.maubis.scarlet.base.support.utils.maybeThrow const val PRIVACY_POLICY_LINK = "https://www.iubenda.com/privacy-policy/8213521" @@ -73,7 +71,7 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { Uri.parse(PRIVACY_POLICY_LINK))) dismiss() }, - visible = ApplicationBase.instance.appFlavor() != Flavor.NONE + visible = FlavorUtils.isPlayStore() )) options.add(LithoOptionsItem( diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index fb9248c3..1f05c315 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -5,7 +5,6 @@ import com.facebook.litho.ComponentContext import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.sheets.openCreateSheet @@ -14,7 +13,7 @@ import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils const val KEY_SECURITY_CODE = "KEY_SECURITY_CODE" const val KEY_FINGERPRINT_ENABLED = "KEY_FINGERPRINT_ENABLED" @@ -54,7 +53,7 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { selected = isPinCodeEnabled() )) - val isLite = instance.appFlavor() == Flavor.LITE + val isLite = FlavorUtils.isLite() options.add(LithoOptionsItem( title = R.string.security_option_lock_app, subtitle = R.string.security_option_lock_app_details, diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index 2e02ca92..c6d6c6fc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -1,8 +1,6 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog -import android.content.Intent -import android.net.Uri import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity @@ -14,10 +12,8 @@ import com.maubis.scarlet.base.note.creation.sheet.EditorOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.utils.Flavor -import com.maubis.scarlet.base.support.utils.FlavourUtils -import com.maubis.scarlet.base.support.utils.FlavourUtils.PRO_APP_PACKAGE_NAME -import com.maubis.scarlet.base.support.utils.FlavourUtils.hasProAppInstalled +import com.maubis.scarlet.base.support.utils.FlavorUtils +import com.maubis.scarlet.base.support.utils.FlavorUtils.PRO_APP_PACKAGE_NAME import com.maubis.scarlet.base.widget.sheet.WidgetOptionsBottomSheet class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { @@ -39,7 +35,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { migrateToPro.function() dismiss() }, - visible = ApplicationBase.instance.appFlavor() == Flavor.LITE && FlavourUtils.hasProAppInstalled(activity), + visible = FlavorUtils.isLite() && FlavorUtils.hasProAppInstalled(activity), selected = true )) options.add(LithoOptionsItem( @@ -109,7 +105,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { IntentUtils.openAppPlayStore(context, PRO_APP_PACKAGE_NAME) dismiss() }, - visible = ApplicationBase.instance.appFlavor() == Flavor.LITE && !hasProAppInstalled(activity) + visible = FlavorUtils.isLite() && !FlavorUtils.hasProAppInstalled(activity) )) options.add(LithoOptionsItem( title = R.string.home_option_rate_and_review, diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index 1e43cba1..92a94cd3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -12,16 +12,18 @@ import com.facebook.litho.annotations.Prop import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.support.sheets.* import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.specs.RoundIcon -import com.maubis.scarlet.base.support.ui.* +import com.maubis.scarlet.base.support.ui.Theme import com.maubis.scarlet.base.support.ui.ThemeManager.Companion.getThemeFromStore -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.ui.sAutomaticTheme +import com.maubis.scarlet.base.support.ui.setThemeFromSystem +import com.maubis.scarlet.base.support.utils.FlavorUtils @LayoutSpec object ThemeColorPickerItemSpec { @@ -90,11 +92,11 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { listener = {}, isSelectable = true, selected = sAutomaticTheme, - actionIcon = if (instance.appFlavor() == Flavor.PRO) 0 else R.drawable.ic_rating + actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 )) .onClick { val context = componentContext.androidContext as AppCompatActivity - if (instance.appFlavor() != Flavor.PRO) { + if (FlavorUtils.isLite()) { openSheet(context, InstallProUpsellBottomSheet()) return@onClick } @@ -120,7 +122,7 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { } val disabled = when { - ApplicationBase.instance.appFlavor() == Flavor.PRO -> false + !FlavorUtils.isLite() -> false theme == Theme.DARK || theme == Theme.LIGHT -> false else -> true } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index c94699df..43ed5083 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -13,7 +13,7 @@ import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet.Companio import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.home_option_ui_experience @@ -68,15 +68,13 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { content = activity.getString(R.string.note_option_font_size_subtitle, sEditorTextSize), icon = R.drawable.ic_title_white_48dp, listener = { - if (flavor == Flavor.PRO) { - com.maubis.scarlet.base.support.sheets.openSheet(activity, FontSizeBottomSheet()) - } else { - openSheet(activity, InstallProUpsellBottomSheet()) + when { + FlavorUtils.isLite() -> openSheet(activity, InstallProUpsellBottomSheet()) + else -> openSheet(activity, FontSizeBottomSheet()) } reset(activity, dialog) }, - visible = flavor != Flavor.NONE, - actionIcon = if (flavor == Flavor.PRO) 0 else R.drawable.ic_rating + actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 )) options.add(LithoOptionsItem( title = R.string.note_option_number_lines, @@ -95,7 +93,7 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, icon = R.drawable.ic_action_color, listener = { - if (flavor != Flavor.PRO) { + if (FlavorUtils.isLite()) { openSheet(activity, InstallProUpsellBottomSheet()) return@LithoOptionsItem } @@ -103,8 +101,7 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { useNoteColorAsBackground = !useNoteColorAsBackground reset(activity, dialog) }, - visible = flavor != Flavor.NONE, - actionIcon = if (flavor == Flavor.PRO) 0 else R.drawable.ic_rating + actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 )) options.add(LithoOptionsItem( title = R.string.markdown_sheet_home_markdown_support, diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt similarity index 76% rename from base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt rename to base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt index ed69650b..088eabd1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavourUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.support.utils import android.content.Context import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -14,7 +15,7 @@ enum class Flavor { PRO, // Play Store Pro Version } -object FlavourUtils { +object FlavorUtils { const val PRO_APP_PACKAGE_NAME = "com.bijoysingh.quicknote.pro" const val KEY_PRO_APP_INSTALLED = "pro_app_installed" fun hasProAppInstalled(context: Context): Boolean { @@ -30,4 +31,9 @@ object FlavourUtils { } return ApplicationBase.instance.store().get(KEY_PRO_APP_INSTALLED, false) } + + fun isPro() = instance.appFlavor() == Flavor.PRO + fun isLite() = instance.appFlavor() == Flavor.LITE + fun isPlayStore() = instance.appFlavor() != Flavor.NONE + fun isOpenSource() = instance.appFlavor() == Flavor.NONE } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index e696e042..fa591c42 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -11,7 +11,6 @@ import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.sort @@ -24,7 +23,7 @@ import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.utils.Flavor +import com.maubis.scarlet.base.support.utils.FlavorUtils import com.maubis.scarlet.base.widget.AllNotesWidgetProvider import com.maubis.scarlet.base.widget.NoteWidgetProvider import kotlinx.coroutines.GlobalScope @@ -154,7 +153,7 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { selected = sWidgetShowToolbar )) - val isLite = instance.appFlavor() == Flavor.LITE + val isLite = FlavorUtils.isLite() options.add(LithoOptionsItem( title = R.string.widget_option_background_color, subtitle = R.string.widget_option_background_color_details, From fe0df5674c8b0567206380d9d56f1c5b1e42f30e Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 2 Oct 2019 21:39:11 +0100 Subject: [PATCH 085/134] [WebDAV][1] Renaming GDrive database to Remote --- .../quicknote/database/GDriveUploadData.kt | 100 --------------- .../database/GDriveUploadDatabase.kt | 21 ---- .../quicknote/database/RemoteUploadData.kt | 116 ++++++++++++++++++ .../database/RemoteUploadDatabase.kt | 21 ++++ .../quicknote/drive/GDriveLoginActivity.kt | 5 - .../drive/GDrivePendingBottomSheet.kt | 20 +-- .../quicknote/drive/GDriveRemoteDatabase.kt | 28 ++--- .../drive/GDriveRemoteDatabaseState.kt | 32 ++--- .../quicknote/drive/GDriveRemoteFolder.kt | 4 +- .../quicknote/drive/GDriveRemoteFolderBase.kt | 31 +++-- .../drive/GDriveRemoteImageFolder.kt | 8 +- .../quicknote/scarlet/ScarletAuthenticator.kt | 3 - 12 files changed, 198 insertions(+), 191 deletions(-) delete mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt delete mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt deleted file mode 100644 index 6049887a..00000000 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadData.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.bijoysingh.quicknote.database - -import android.arch.persistence.room.* -import com.google.gson.Gson -import com.maubis.scarlet.base.support.utils.log -import com.maubis.scarlet.base.support.utils.maybeThrow - -enum class GDriveDataType { - NOTE, - NOTE_META, - TAG, - FOLDER, - IMAGE, -} - -@Entity(tableName = "gdrive_upload", indices = [Index("uuid", "type", unique = true)]) -class GDriveUploadData { - @PrimaryKey(autoGenerate = true) - var uid: Int = 0 - - var uuid: String = "" - - var type: String = "" - - var fileId: String = "" - - var lastUpdateTimestamp: Long = 0L - - var localStateDeleted: Boolean = false - - var gDriveUpdateTimestamp: Long = 0L - - var gDriveStateDeleted: Boolean = false - - var attempts: Long = 0L - - var lastAttemptTime: Long = 0L - - @Ignore - fun save(dao: GDriveUploadDataDao) { - if (uuid.isBlank() || type.isBlank()) { - maybeThrow("Invalid Dao") - return - } - - log("GDrive", "data = ${Gson().toJson(this)}") - val id = dao.insert(this) - uid = if (uid == 0) id.toInt() else uid - } - - @Ignore - fun unsaved(): Boolean { - return uid == 0 - } -} - -object GDriveDatabaseHelper { - fun getByUUID(type: GDriveDataType, uuid: String): GDriveUploadData { - return getByUUID(type.name, uuid) - } - - fun getByUUID(itemType: String, itemUuid: String): GDriveUploadData { - return gDriveDatabase?.getByUUID(itemType, itemUuid) ?: GDriveUploadData().apply { - this.uuid = itemUuid - this.type = itemType - } - }} - -@Dao -interface GDriveUploadDataDao { - @get:Query("SELECT * FROM gdrive_upload") - val all: List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(note: GDriveUploadData): Long - - @Query("UPDATE gdrive_upload SET attempts = 0") - fun resetAttempts() - - @Delete - fun delete(note: GDriveUploadData) - - @Query("SELECT * FROM gdrive_upload WHERE uid = :uid LIMIT 1") - fun getByID(uid: Int): GDriveUploadData? - - @Query("SELECT * FROM gdrive_upload WHERE uuid = :uuid AND type = :type LIMIT 1") - fun getByUUID(type: String, uuid: String): GDriveUploadData? - - @Query("SELECT * FROM gdrive_upload WHERE type = :type") - fun getByType(type: String): List - - @Query("SELECT COUNT(*) FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") - fun getPendingCount(): Int - - @Query("SELECT * FROM gdrive_upload WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") - fun getAllPending(): List - - @Query("SELECT * FROM gdrive_upload WHERE type = :type AND (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") - fun getPendingByType(type: String): List -} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt deleted file mode 100644 index fe561d4f..00000000 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/GDriveUploadDatabase.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.bijoysingh.quicknote.database - -import android.arch.persistence.room.Database -import android.arch.persistence.room.Room -import android.arch.persistence.room.RoomDatabase -import android.content.Context - -var gDriveDatabase: GDriveUploadDataDao? = null -fun genGDriveUploadDatabase(context: Context): GDriveUploadDataDao? { - if (gDriveDatabase === null) { - gDriveDatabase = Room.databaseBuilder(context, GDriveUploadDatabase::class.java, "google_drive_db") - .fallbackToDestructiveMigration() - .build().drive() - } - return gDriveDatabase -} - -@Database(entities = [GDriveUploadData::class], version = 4) -abstract class GDriveUploadDatabase : RoomDatabase() { - abstract fun drive(): GDriveUploadDataDao -} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt new file mode 100644 index 00000000..9bf9337d --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt @@ -0,0 +1,116 @@ +package com.bijoysingh.quicknote.database + +import android.arch.persistence.room.* +import com.google.gson.Gson +import com.maubis.scarlet.base.support.utils.log +import com.maubis.scarlet.base.support.utils.maybeThrow + +enum class GDriveDataType { + NOTE, + NOTE_META, + TAG, + FOLDER, + IMAGE, +} + +@Entity(tableName = "gdrive_upload", indices = [Index("uuid", "type", unique = true)]) +class RemoteUploadData { + @PrimaryKey(autoGenerate = true) + var uid: Int = 0 + + var uuid: String = "" + + var type: String = "" + + var fileId: String = "" + + var lastUpdateTimestamp: Long = 0L + + var localStateDeleted: Boolean = false + + @ColumnInfo(name="gDriveUpdateTimestamp") + var remoteUpdateTimestamp: Long = 0L + + @ColumnInfo(name="gDriveStateDeleted") + var remoteStateDeleted: Boolean = false + + var attempts: Long = 0L + + var lastAttemptTime: Long = 0L + + @Ignore + fun save(dao: RemoteUploadDataDao) { + if (uuid.isBlank() || type.isBlank()) { + maybeThrow("Invalid Dao") + return + } + + log("GDrive", "data = ${Gson().toJson(this)}") + val id = dao.insert(this) + uid = if (uid == 0) id.toInt() else uid + } + + @Ignore + fun unsaved(): Boolean { + return uid == 0 + } +} + +object RemoteDatabaseHelper { + fun getByUUID(type: GDriveDataType, uuid: String): RemoteUploadData { + return getByUUID(type.name, uuid) + } + + fun getByUUID(itemType: String, itemUuid: String): RemoteUploadData { + return remoteDatabase?.getByUUID(itemType, itemUuid) ?: RemoteUploadData().apply { + this.uuid = itemUuid + this.type = itemType + } + }} + +@Dao +interface RemoteUploadDataDao { + @get:Query("SELECT * FROM gdrive_upload") + val all: List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(note: RemoteUploadData): Long + + @Query("UPDATE gdrive_upload SET attempts = 0") + fun resetAttempts() + + @Delete + fun delete(note: RemoteUploadData) + + @Query("SELECT * " + + "FROM gdrive_upload " + + "WHERE uid = :uid " + + "LIMIT 1") + fun getByID(uid: Int): RemoteUploadData? + + @Query("SELECT * " + + "FROM gdrive_upload " + + "WHERE uuid = :uuid AND type = :type " + + "LIMIT 1") + fun getByUUID(type: String, uuid: String): RemoteUploadData? + + @Query("SELECT * " + + "FROM gdrive_upload " + + "WHERE type = :type") + fun getByType(type: String): List + + @Query("SELECT COUNT(*) " + + "FROM gdrive_upload " + + "WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getPendingCount(): Int + + @Query("SELECT * " + + "FROM gdrive_upload " + + "WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getAllPending(): List + + @Query("SELECT * " + + "FROM gdrive_upload " + + "WHERE type = :type AND (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") + fun getPendingByType(type: String): List +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt new file mode 100644 index 00000000..17e1a42f --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt @@ -0,0 +1,21 @@ +package com.bijoysingh.quicknote.database + +import android.arch.persistence.room.Database +import android.arch.persistence.room.Room +import android.arch.persistence.room.RoomDatabase +import android.content.Context + +var remoteDatabase: RemoteUploadDataDao? = null +fun genRemoteDatabase(context: Context): RemoteUploadDataDao? { + if (remoteDatabase === null) { + remoteDatabase = Room.databaseBuilder(context, RemoteUploadDatabase::class.java, "google_drive_db") + .fallbackToDestructiveMigration() + .build().remote() + } + return remoteDatabase +} + +@Database(entities = [RemoteUploadData::class], version = 4) +abstract class RemoteUploadDatabase : RoomDatabase() { + abstract fun remote(): RemoteUploadDataDao +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index c9351294..50276882 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -5,9 +5,6 @@ import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState -import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadData import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.facebook.litho.Component @@ -28,9 +25,7 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.support.ui.SecuredActivity -import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index d8713649..db5a9015 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -4,8 +4,8 @@ import android.app.Dialog import android.graphics.drawable.Drawable import android.text.TextUtils import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadData -import com.bijoysingh.quicknote.database.gDriveDatabase +import com.bijoysingh.quicknote.database.RemoteUploadData +import com.bijoysingh.quicknote.database.remoteDatabase import com.facebook.litho.* import com.facebook.litho.annotations.* import com.facebook.litho.widget.Image @@ -31,7 +31,7 @@ import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean data class PendingItem( - val state: GDriveUploadData, + val state: RemoteUploadData, val info: String? ) @@ -91,7 +91,7 @@ object PendingItemLayoutSpec { } val localState = when { option.state.lastUpdateTimestamp == 0L -> R.string.pending_backup_state_unavailable - option.state.gDriveUpdateTimestamp == 0L && !option.state.localStateDeleted -> R.string.pending_backup_state_created + option.state.remoteUpdateTimestamp == 0L && !option.state.localStateDeleted -> R.string.pending_backup_state_created option.state.localStateDeleted -> R.string.pending_backup_state_deleted else -> R.string.pending_backup_state_updated } @@ -101,14 +101,14 @@ object PendingItemLayoutSpec { } val remoteState = when { - option.state.gDriveUpdateTimestamp == 0L -> R.string.pending_backup_state_unavailable - option.state.lastUpdateTimestamp == 0L && !option.state.gDriveStateDeleted -> R.string.pending_backup_state_created - option.state.gDriveStateDeleted -> R.string.pending_backup_state_deleted + option.state.remoteUpdateTimestamp == 0L -> R.string.pending_backup_state_unavailable + option.state.lastUpdateTimestamp == 0L && !option.state.remoteStateDeleted -> R.string.pending_backup_state_created + option.state.remoteStateDeleted -> R.string.pending_backup_state_deleted else -> R.string.pending_backup_state_updated } val remoteUpdateTime = when { - option.state.gDriveUpdateTimestamp == 0L -> "" - else -> DateFormatter.getDate(DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, option.state.gDriveUpdateTimestamp) + option.state.remoteUpdateTimestamp == 0L -> "" + else -> DateFormatter.getDate(DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, option.state.remoteUpdateTimestamp) } val column = Column.create(context) @@ -221,7 +221,7 @@ class GDrivePendingBottomSheet : LithoBottomSheet() { fun requestData(onDataAvailable: () -> Unit) { GlobalScope.launch { data.clear() - gDriveDatabase?.getAllPending()?.forEach { + remoteDatabase?.getAllPending()?.forEach { val pendingItem = when (it.type) { GDriveDataType.NOTE.name -> PendingItem(state = it, info = instance.notesDatabase().getByUUID(it.uuid)?.getFullText()) GDriveDataType.NOTE_META.name -> { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 4b2c0d87..b785c3e8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -4,9 +4,9 @@ import android.content.Context import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadData -import com.bijoysingh.quicknote.database.GDriveUploadDataDao -import com.bijoysingh.quicknote.database.genGDriveUploadDatabase +import com.bijoysingh.quicknote.database.RemoteUploadData +import com.bijoysingh.quicknote.database.RemoteUploadDataDao +import com.bijoysingh.quicknote.database.genRemoteDatabase import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase @@ -82,7 +82,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { lateinit var gDriveDbState: GDriveRemoteDatabaseState - private var gDriveDatabase: GDriveUploadDataDao? = null + private var gDriveDatabase: RemoteUploadDataDao? = null private var isValidController: Boolean = true private var driveHelper: GDriveServiceHelper? = null @@ -104,7 +104,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { isValidController = true driveHelper = helper - gDriveDatabase = genGDriveUploadDatabase(context) + gDriveDatabase = genRemoteDatabase(context) gDriveDbState = Scarlet.gDriveDbState!! syncing[GDriveDataType.NOTE] = AtomicBoolean(false) @@ -374,12 +374,12 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { continue } - val sameDelete = pendingItem.localStateDeleted == pendingItem.gDriveStateDeleted + val sameDelete = pendingItem.localStateDeleted == pendingItem.remoteStateDeleted val localDeleted = pendingItem.localStateDeleted - val remoteDeleted = pendingItem.gDriveStateDeleted - val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.gDriveUpdateTimestamp - val isLocalMoreRecent = pendingItem.lastUpdateTimestamp > pendingItem.gDriveUpdateTimestamp - val isRemoteMoreRecent = pendingItem.lastUpdateTimestamp < pendingItem.gDriveUpdateTimestamp + val remoteDeleted = pendingItem.remoteStateDeleted + val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.remoteUpdateTimestamp + val isLocalMoreRecent = pendingItem.lastUpdateTimestamp > pendingItem.remoteUpdateTimestamp + val isRemoteMoreRecent = pendingItem.lastUpdateTimestamp < pendingItem.remoteUpdateTimestamp when { sameUpdateTime -> gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) !sameDelete && isRemoteMoreRecent && remoteDeleted -> onRemoteRemove(type, pendingItem) @@ -400,7 +400,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { */ @Suppress("IMPLICIT_CAST_TO_ANY") - private fun insert(type: GDriveDataType, data: GDriveUploadData) { + private fun insert(type: GDriveDataType, data: RemoteUploadData) { if (!isValidController) { return } @@ -443,7 +443,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } - private fun remove(type: GDriveDataType, data: GDriveUploadData) { + private fun remove(type: GDriveDataType, data: RemoteUploadData) { if (!isValidController) { return } @@ -465,7 +465,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } - private fun onRemoteInsert(type: GDriveDataType, data: GDriveUploadData) { + private fun onRemoteInsert(type: GDriveDataType, data: RemoteUploadData) { if (!isValidController) { return } @@ -552,7 +552,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } - private fun onRemoteRemove(type: GDriveDataType, data: GDriveUploadData) { + private fun onRemoteRemove(type: GDriveDataType, data: RemoteUploadData) { if (!isValidController) { return } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt index c1908100..f1b84016 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt @@ -2,9 +2,9 @@ package com.bijoysingh.quicknote.drive import android.content.Context import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveDatabaseHelper -import com.bijoysingh.quicknote.database.gDriveDatabase -import com.bijoysingh.quicknote.database.genGDriveUploadDatabase +import com.bijoysingh.quicknote.database.RemoteDatabaseHelper +import com.bijoysingh.quicknote.database.remoteDatabase +import com.bijoysingh.quicknote.database.genRemoteDatabase import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { init { - gDriveDatabase = genGDriveUploadDatabase(context) + remoteDatabase = genRemoteDatabase(context) } /** @@ -35,7 +35,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { } fun notifyInsertIfNotPresent(data: Any) { - val database = gDriveDatabase + val database = remoteDatabase if (database === null) { return } @@ -66,7 +66,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { localDatabaseUpdate(GDriveDataType.NOTE, noteUuid, onExecution) localDatabaseUpdate(GDriveDataType.NOTE_META, noteUuid, onExecution) - val database = gDriveDatabase + val database = remoteDatabase if (database === null) { return } @@ -131,12 +131,12 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { * @return if false, the item will be deleted from the database */ fun notifyAttempt(itemType: GDriveDataType, itemUUID: String): Boolean { - val database = gDriveDatabase + val database = remoteDatabase if (database === null) { return false } - val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + val existing = RemoteDatabaseHelper.getByUUID(itemType, itemUUID) val lastAttemptedTime = existing.lastAttemptTime // If it fails 8 times, only re-attempt after hour. This handles situations like no-network conditions @@ -155,18 +155,18 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { fun remoteDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, onExecution: () -> Unit) { GlobalScope.launch { - val database = gDriveDatabase + val database = remoteDatabase if (database === null) { return@launch } log("GDrive", "remoteDatabaseUpdate(${itemType.name}, $itemUUID)") - val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + val existing = RemoteDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { attempts = 0 lastAttemptTime = 0 - lastUpdateTimestamp = gDriveUpdateTimestamp - localStateDeleted = gDriveStateDeleted + lastUpdateTimestamp = remoteUpdateTimestamp + localStateDeleted = remoteStateDeleted save(database) } onExecution() @@ -179,17 +179,17 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { onExecution: () -> Unit, removed: Boolean = false) { GlobalScope.launch { - val database = gDriveDatabase + val database = remoteDatabase if (database === null) { return@launch } log("GDrive", "localDatabaseUpdate(${itemType.name}, $itemUUID)") - val existing = GDriveDatabaseHelper.getByUUID(itemType, itemUUID) + val existing = RemoteDatabaseHelper.getByUUID(itemType, itemUUID) existing.apply { attempts = 0 lastAttemptTime = 0 - lastUpdateTimestamp = Math.max(Math.max(gDriveUpdateTimestamp + 1, lastUpdateTimestamp + 1), getTrueCurrentTime()) + lastUpdateTimestamp = Math.max(Math.max(remoteUpdateTimestamp + 1, lastUpdateTimestamp + 1), getTrueCurrentTime()) localStateDeleted = removed save(database) } @@ -202,7 +202,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { itemUUID: String, onExecution: () -> Unit) { GlobalScope.launch { - val database = gDriveDatabase + val database = remoteDatabase if (database !== null) { val existing = database.getByUUID(itemType.name, itemUUID) if (existing !== null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 3a1521a5..63e09e08 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -1,7 +1,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.bijoysingh.quicknote.database.RemoteUploadDataDao import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -10,7 +10,7 @@ import java.util.concurrent.atomic.AtomicBoolean class GDriveRemoteFolder( dataType: GDriveDataType, - database: GDriveUploadDataDao, + database: RemoteUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit, val serialiser: (T) -> String, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 00ad7143..012808a9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -1,16 +1,15 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveDatabaseHelper -import com.bijoysingh.quicknote.database.GDriveUploadData -import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.bijoysingh.quicknote.database.RemoteDatabaseHelper +import com.bijoysingh.quicknote.database.RemoteUploadDataDao import com.google.api.services.drive.model.File import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch abstract class GDriveRemoteFolderBase( val dataType: GDriveDataType, - val database: GDriveUploadDataDao, + val database: RemoteUploadDataDao, val helper: GDriveServiceHelper, val onPendingChange: () -> Unit) { @@ -21,47 +20,47 @@ abstract class GDriveRemoteFolderBase( protected fun notifyDriveData(uid: String, name: String, modifiedTime: Long, deleted: Boolean = false) { GlobalScope.launch { - val uploadData = GDriveDatabaseHelper.getByUUID(dataType, name) + val uploadData = RemoteDatabaseHelper.getByUUID(dataType, name) if (uploadData.unsaved()) { uploadData.apply { fileId = uid - gDriveUpdateTimestamp = modifiedTime - gDriveStateDeleted = deleted + remoteUpdateTimestamp = modifiedTime + remoteStateDeleted = deleted save(database) } onPendingChange() return@launch } - if (uploadData.gDriveStateDeleted != deleted) { + if (uploadData.remoteStateDeleted != deleted) { uploadData.apply { - gDriveUpdateTimestamp = modifiedTime + remoteUpdateTimestamp = modifiedTime fileId = uid - gDriveStateDeleted = deleted + remoteStateDeleted = deleted save(database) } onPendingChange() return@launch } - if (uploadData.fileId != uid && uploadData.gDriveUpdateTimestamp < modifiedTime) { + if (uploadData.fileId != uid && uploadData.remoteUpdateTimestamp < modifiedTime) { // This is a bit of shit situation to be in, as multiple files are pointing to the // same name... Something Google Drive allows for whatever reason // To help disambiguate, choose the one with the higher update timestamp uploadData.apply { - gDriveUpdateTimestamp = modifiedTime + remoteUpdateTimestamp = modifiedTime fileId = uid - gDriveStateDeleted = deleted + remoteStateDeleted = deleted save(database) } onPendingChange() } - if (uploadData.fileId == uid && uploadData.gDriveUpdateTimestamp != modifiedTime) { + if (uploadData.fileId == uid && uploadData.remoteUpdateTimestamp != modifiedTime) { uploadData.apply { - gDriveUpdateTimestamp = modifiedTime + remoteUpdateTimestamp = modifiedTime fileId = uid - gDriveStateDeleted = deleted + remoteStateDeleted = deleted save(database) } onPendingChange() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 341bdbfe..3ac5ed13 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -1,7 +1,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.GDriveUploadDataDao +import com.bijoysingh.quicknote.database.RemoteUploadDataDao import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -39,7 +39,7 @@ fun toImageUUID(imageUuid: String): ImageUUID? { class GDriveRemoteImageFolder( dataType: GDriveDataType, - database: GDriveUploadDataDao, + database: RemoteUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { @@ -89,8 +89,8 @@ class GDriveRemoteImageFolder( if (contentFiles.containsKey(id)) { GlobalScope.launch { database.getByUUID(dataType.name, id.name())?.apply { - gDriveUpdateTimestamp = lastUpdateTimestamp - gDriveStateDeleted = localStateDeleted + remoteUpdateTimestamp = lastUpdateTimestamp + remoteStateDeleted = localStateDeleted save(database) } onPendingChange() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 739a246c..be2c91e7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.database.gDriveDatabase import com.bijoysingh.quicknote.drive.* import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity @@ -13,8 +12,6 @@ import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemedActivity -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" var sGDriveLoggedIn: Boolean From 3fccd0d7570c3f7739ba54ecd9c9e253a98e4b71 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 2 Oct 2019 23:36:10 +0100 Subject: [PATCH 086/134] [WebDAV][2] Adding More abstractions to support WebDAV --- .../4.json | 97 ++++ .../java/com/bijoysingh/quicknote/Scarlet.kt | 5 +- .../quicknote/database/OnRemoteCompletion.kt | 4 + .../quicknote/database/RemoteController.kt | 485 ++++++++++++++++++ .../RemoteDatabaseStateController.kt} | 57 +- .../quicknote/database/RemoteFolder.kt | 14 + .../quicknote/database/RemoteService.kt | 17 + .../quicknote/database/RemoteUploadData.kt | 4 +- .../quicknote/drive/GDriveAuthenticator.kt | 1 - .../quicknote/drive/GDriveLoginActivity.kt | 3 +- .../drive/GDrivePendingBottomSheet.kt | 32 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 115 ++--- .../quicknote/drive/GDriveRemoteFolder.kt | 4 +- .../quicknote/drive/GDriveRemoteFolderBase.kt | 4 +- .../drive/GDriveRemoteImageFolder.kt | 4 +- .../quicknote/scarlet/ScarletAuthenticator.kt | 3 +- .../quicknote/scarlet/ScarletConfig.kt | 5 +- .../quicknote/scarlet/ScarletFolderActor.kt | 8 +- .../quicknote/scarlet/ScarletNoteActor.kt | 8 +- .../quicknote/scarlet/ScarletTagActor.kt | 9 +- 20 files changed, 746 insertions(+), 133 deletions(-) create mode 100644 scarlet/schemas/com.bijoysingh.quicknote.database.RemoteUploadDatabase/4.json create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/OnRemoteCompletion.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt rename scarlet/src/main/java/com/bijoysingh/quicknote/{drive/GDriveRemoteDatabaseState.kt => database/RemoteDatabaseStateController.kt} (76%) create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt create mode 100644 scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt diff --git a/scarlet/schemas/com.bijoysingh.quicknote.database.RemoteUploadDatabase/4.json b/scarlet/schemas/com.bijoysingh.quicknote.database.RemoteUploadDatabase/4.json new file mode 100644 index 00000000..c5e37325 --- /dev/null +++ b/scarlet/schemas/com.bijoysingh.quicknote.database.RemoteUploadDatabase/4.json @@ -0,0 +1,97 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "a64a14da15936f694f2119342656f35e", + "entities": [ + { + "tableName": "gdrive_upload", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `fileId` TEXT NOT NULL, `lastUpdateTimestamp` INTEGER NOT NULL, `localStateDeleted` INTEGER NOT NULL, `gDriveUpdateTimestamp` INTEGER NOT NULL, `gDriveStateDeleted` INTEGER NOT NULL, `attempts` INTEGER NOT NULL, `lastAttemptTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdateTimestamp", + "columnName": "lastUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localStateDeleted", + "columnName": "localStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteUpdateTimestamp", + "columnName": "gDriveUpdateTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteStateDeleted", + "columnName": "gDriveStateDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attempts", + "columnName": "attempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastAttemptTime", + "columnName": "lastAttemptTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_gdrive_upload_uuid_type", + "unique": true, + "columnNames": [ + "uuid", + "type" + ], + "createSql": "CREATE UNIQUE INDEX `index_gdrive_upload_uuid_type` ON `${TABLE_NAME}` (`uuid`, `type`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a64a14da15936f694f2119342656f35e\")" + ] + } +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index d5bccd24..dcb1f51b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -1,7 +1,7 @@ package com.bijoysingh.quicknote import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase -import com.bijoysingh.quicknote.drive.GDriveRemoteDatabaseState +import com.bijoysingh.quicknote.database.RemoteDatabaseStateController import com.bijoysingh.quicknote.firebase.FirebaseRemoteDatabase import com.bijoysingh.quicknote.scarlet.ScarletConfig import com.github.bijoysingh.starter.prefs.Store @@ -25,7 +25,8 @@ class Scarlet : ApplicationBase() { companion object { var firebase: FirebaseRemoteDatabase? = null var gDrive: GDriveRemoteDatabase? = null - var gDriveDbState: GDriveRemoteDatabaseState? = null var gDriveConfig: Store? = null + + var remoteDatabaseStateController: RemoteDatabaseStateController? = null } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/OnRemoteCompletion.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/OnRemoteCompletion.kt new file mode 100644 index 00000000..9e08f7ea --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/OnRemoteCompletion.kt @@ -0,0 +1,4 @@ +package com.bijoysingh.quicknote.database + +interface OnRemoteCompletion { +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt new file mode 100644 index 00000000..2228540b --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt @@ -0,0 +1,485 @@ +package com.bijoysingh.quicknote.database + +import android.content.Context +import com.bijoysingh.quicknote.Scarlet +import com.bijoysingh.quicknote.drive.* +import com.bijoysingh.quicknote.firebase.data.getFirebaseNote +import com.google.gson.Gson +import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.auth.IPendingUploadListener +import com.maubis.scarlet.base.core.note.NoteBuilder +import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils +import com.maubis.scarlet.base.export.data.* +import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.support.utils.log +import com.maubis.scarlet.base.support.utils.maybeThrow +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.File +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicBoolean + +abstract class RemoteController(private val weakContext: WeakReference) { + + private val isValidController: AtomicBoolean = AtomicBoolean(true) + + lateinit var remoteDatabaseController: RemoteDatabaseStateController + lateinit var remoteService: IRemoteService + lateinit var remoteDatabase: RemoteUploadDataDao + + lateinit var notesSync: RemoteFolder + lateinit var notesMetaSync: RemoteFolder + lateinit var foldersSync: RemoteFolder + lateinit var tagsSync: RemoteFolder + lateinit var imageSync: RemoteFolder + + private var syncing = HashMap() + private var syncListener: IPendingUploadListener? = null + private var databaseUpdateLambda: () -> Unit = { verifyAndNotifyPendingStateChange() } + + fun init(service: IRemoteService) { + val context = weakContext.get() + if (context === null) { + return + } + + isValidController.set(true) + remoteService = service + remoteDatabase = genRemoteDatabase(context)!! + + syncing[RemoteDataType.NOTE] = AtomicBoolean(false) + syncing[RemoteDataType.TAG] = AtomicBoolean(false) + syncing[RemoteDataType.FOLDER] = AtomicBoolean(false) + syncing[RemoteDataType.NOTE_META] = AtomicBoolean(false) + syncing[RemoteDataType.IMAGE] = AtomicBoolean(false) + + initSyncs() + initRootFolder() + } + + private fun isValid(): Boolean { + return isValidController.get() + } + + fun reset() { + isValidController.set(false) + + notesSync.invalidate() + foldersSync.invalidate() + tagsSync.invalidate() + imageSync.invalidate() + } + + + fun logout() { + GlobalScope.launch { + reset() + Scarlet.gDriveConfig?.clearSync() + } + } + + + /** + * Abstract Methods + */ + + abstract fun initSyncs() + abstract fun getResourceIdForFolderName(folderName: String): T? + abstract fun storeResourceIdForFolderName(folderName: String, resource: T) + + /** + * Initialisation Methods + */ + + private fun initRootFolder() { + GlobalScope.launch { + sGDriveLastFullSyncTime = getTrueCurrentTime() + val fuid = getResourceIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) + when { + fuid !== null -> onRootFolderLoaded(fuid) + else -> { + remoteService.getOrCreateDirectory(null, GOOGLE_DRIVE_ROOT_FOLDER) { folderResource -> + when { + (folderResource === null) -> reset() + else -> { + storeResourceIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER, folderResource) + onRootFolderLoaded(folderResource) + } + } + } + } + } + } + } + + private fun onRootFolderLoaded(rootFolderId: T) { + createFolders(rootFolderId, listOf(FOLDER_NAME_NOTES, FOLDER_NAME_NOTES_META, FOLDER_NAME_FOLDERS, FOLDER_NAME_TAGS, FOLDER_NAME_IMAGES)) + createFolders(rootFolderId, listOf(FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS)) + } + + private fun createFolders(rootFolderId: T, expectedFolders: List) { + val knownFolderIds = expectedFolders.filter { getResourceIdForFolderName(it) !== null } + knownFolderIds.forEach { + GlobalScope.launch { initSubRootFolder(it, getResourceIdForFolderName(it)!!) } + } + + val unknownFolderNames = expectedFolders.filter { !knownFolderIds.contains(it) } + if (unknownFolderNames.isEmpty()) { + return + } + + remoteService.getDirectories(rootFolderId, unknownFolderNames) { items -> + val pendingNames = emptyList().toMutableList() + items.forEach { pair -> + val name = pair.first + val resourceId = pair.second + when { + resourceId !== null -> { + storeResourceIdForFolderName(name, resourceId) + initSubRootFolder(name, resourceId) + } + else -> pendingNames.add(name) + } + } + pendingNames.forEach { name -> + remoteService.createDirectory(rootFolderId, name) { folderId -> + if (folderId !== null) { + storeResourceIdForFolderName(name, folderId) + GlobalScope.launch { initSubRootFolder(name, folderId) } + } + } + } + } + } + + private fun initSubRootFolder(folderName: String, folderId: T) { + when (folderName) { + FOLDER_NAME_NOTES -> notesSync.initContentFolder(folderId) { + if (!sGDriveFirstSyncNote) { + GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE) } + sGDriveFirstSyncNote = true + } + } + FOLDER_NAME_NOTES_META -> { + notesMetaSync.initContentFolder(folderId) { + if (!sGDriveFirstSyncNoteMeta) { + GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE_META) } + sGDriveFirstSyncNoteMeta = true + } + } + notesMetaSync.initDeletedFolder(null) {} + } + FOLDER_NAME_TAGS -> tagsSync.initContentFolder(folderId) { + if (!sGDriveFirstSyncTag) { + GlobalScope.launch { resyncDataSync(RemoteDataType.TAG) } + sGDriveFirstSyncTag = true + } + } + FOLDER_NAME_FOLDERS -> foldersSync.initContentFolder(folderId) { + if (!sGDriveFirstSyncFolder) { + GlobalScope.launch { resyncDataSync(RemoteDataType.FOLDER) } + sGDriveFirstSyncFolder = true + } + } + FOLDER_NAME_IMAGES -> imageSync.initContentFolder(folderId) { + if (!sGDriveFirstSyncImage) { + GlobalScope.launch { resyncDataSync(RemoteDataType.IMAGE) } + sGDriveFirstSyncImage = true + } + } + FOLDER_NAME_DELETED_NOTES -> notesSync.initDeletedFolder(folderId) { + + } + FOLDER_NAME_DELETED_TAGS -> tagsSync.initDeletedFolder(folderId) { + + } + FOLDER_NAME_DELETED_FOLDERS -> foldersSync.initDeletedFolder(folderId) { + + } + } + } + + /** + * Pending Upload + */ + + fun setPendingUploadListener(listener: IPendingUploadListener?) { + syncListener = listener + if (listener !== null) { + verifyAndNotifyPendingStateChange() + } + } + + private fun verifyAndNotifyPendingStateChange() { + GlobalScope.launch { + val currentPendingState = remoteDatabase.getPendingCount() > 0 + val pending = remoteDatabase.getAllPending().map { "type=${it.type}, uuid=${it.uuid}, fid=${it.fileId}" }.joinToString(separator = "\n") + log("GDrive", "getPendingCount(${remoteDatabase.getPendingCount()})\n$pending") + notifyPendingSyncChange("verifyAndNotifyPendingStateChange") + syncListener?.onPendingStateUpdate(currentPendingState) + } + } + + fun notifyPendingSyncChange(action: String) { + val count = sSyncingCount.get() + when { + count <= 0 -> syncListener?.onPendingSyncsUpdate(false) + count >= 1 -> syncListener?.onPendingSyncsUpdate(true) + } + } + + /** + * Notify local changes to the notes + */ + fun notifyChange() { + if (!isValid()) { + return + } + + verifyAndNotifyPendingStateChange() + } + + /** + * Resync Functions + */ + + @Synchronized + fun resync(forced: Boolean) { + if (!isValid()) { + return + } + + if (forced || (getTrueCurrentTime() - sGDriveLastFullSyncTime > 1000 * 60 * 60 * 24)) { + GlobalScope.launch { + remoteDatabase.resetAttempts() + initRootFolder() + } + return + } + + GlobalScope.launch { + resyncDataSync(RemoteDataType.NOTE) + resyncDataSync(RemoteDataType.NOTE_META) + resyncDataSync(RemoteDataType.TAG) + resyncDataSync(RemoteDataType.FOLDER) + resyncDataSync(RemoteDataType.IMAGE) + } + } + + private fun resyncDataSync(type: RemoteDataType) { + if (syncing[type]?.getAndSet(true) == true) { + return + } + + val pendingItems = remoteDatabase.getPendingByType(type.name) + for (pendingItem in pendingItems) { + if (!remoteDatabaseController.notifyAttempt(type, pendingItem.uuid)) { + continue + } + + val sameDelete = pendingItem.localStateDeleted == pendingItem.remoteStateDeleted + val localDeleted = pendingItem.localStateDeleted + val remoteDeleted = pendingItem.remoteStateDeleted + val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.remoteUpdateTimestamp + val isLocalMoreRecent = pendingItem.lastUpdateTimestamp > pendingItem.remoteUpdateTimestamp + val isRemoteMoreRecent = pendingItem.lastUpdateTimestamp < pendingItem.remoteUpdateTimestamp + when { + sameUpdateTime -> remoteDatabaseController.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) + !sameDelete && isRemoteMoreRecent && remoteDeleted -> onRemoteRemove(type, pendingItem) + !sameDelete && isLocalMoreRecent && localDeleted -> remove(type, pendingItem) + !sameDelete && isLocalMoreRecent && remoteDeleted -> insert(type, pendingItem) + !sameDelete && isRemoteMoreRecent && localDeleted -> onRemoteInsert(type, pendingItem) + sameDelete && isLocalMoreRecent && localDeleted -> remove(type, pendingItem) + sameDelete && isLocalMoreRecent && !localDeleted -> insert(type, pendingItem) + sameDelete && isRemoteMoreRecent && localDeleted -> onRemoteRemove(type, pendingItem) + sameDelete && isRemoteMoreRecent && !localDeleted -> onRemoteInsert(type, pendingItem) + } + } + syncing[type]?.set(false) + } + + /** + * Core Data Functions + */ + + @Suppress("IMPLICIT_CAST_TO_ANY") + private fun insert(type: RemoteDataType, data: RemoteUploadData) { + if (!isValid()) { + return + } + + val localItem = when (type) { + RemoteDataType.NOTE -> { + ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { + notesSync.insert(data, this) + } + } + RemoteDataType.NOTE_META -> { + ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { + notesMetaSync.insert(data, this) + } + } + RemoteDataType.TAG -> { + ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { + tagsSync.insert(data, this) + } + } + RemoteDataType.FOLDER -> { + ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { + foldersSync.insert(data, this) + } + } + RemoteDataType.IMAGE -> { + val imageUUID = toImageUUID(data.uuid) + when { + imageUUID !== null -> noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid).apply { + imageSync.insert(data, this) + } + else -> null + } + } + } + + if (localItem === null) { + remoteDatabaseController.localDatabaseRemove(type, data.uuid) {} + } + } + + private fun remove(type: RemoteDataType, data: RemoteUploadData) { + if (!isValid()) { + return + } + + val logInfo = "remove(${type.name}, ${data.uuid})" + log("GDriveRemote", logInfo) + when (type) { + RemoteDataType.NOTE -> notesSync.delete(data) + RemoteDataType.NOTE_META -> notesMetaSync.delete(data) + RemoteDataType.TAG -> tagsSync.delete(data) + RemoteDataType.FOLDER -> foldersSync.delete(data) + RemoteDataType.IMAGE -> imageSync.delete(data) + } + } + + private fun onRemoteInsert(type: RemoteDataType, data: RemoteUploadData) { + if (!isValid()) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + + when (type) { + RemoteDataType.NOTE -> { + remoteService.readFile(data) { content -> + // TODO: De-duplicate meta data and note update + try { + val itemDescription = fromExportedMarkdown(content) + val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) + ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } + val temporaryNote = NoteBuilder().copy(existingNote) + temporaryNote.description = itemDescription + IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) + + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.NOTE, data.uuid, databaseUpdateLambda) + } catch (exception: Exception) { + maybeThrow(exception) + } + } + } + RemoteDataType.NOTE_META -> { + // TODO: De-duplicate meta data and note update + remoteService.readFile(data) { content -> + try { + val item = Gson().fromJson(content, ExportableNoteMeta::class.java) + + val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) + ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } + val temporaryNote = NoteBuilder().copy(existingNote) + temporaryNote.mergeMetas(item) + IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) + + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, databaseUpdateLambda) + } catch (exception: Exception) { + maybeThrow(exception) + } + } + } + RemoteDataType.TAG -> { + remoteService.readFile(data) { content -> + try { + val item = Gson().fromJson(content, ExportableTag::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.TAG, data.uuid, databaseUpdateLambda) + } catch (exception: Exception) { + maybeThrow(exception) + } + } + } + RemoteDataType.FOLDER -> { + remoteService.readFile(data) { content -> + try { + val item = Gson().fromJson(content, ExportableFolder::class.java) + IRemoteDatabaseUtils.onRemoteInsert(context, item) + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.FOLDER, data.uuid, databaseUpdateLambda) + } catch (exception: Exception) { + maybeThrow(exception) + } + } + } + RemoteDataType.IMAGE -> { + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null) { + val imageFile = ApplicationBase.noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + if (imageFile.exists()) { + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) + return + } + + remoteService.readIntoFile(data, imageFile) { success -> + if (success) { + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) + } + } + } + } + } + } + + private fun onRemoteRemove(type: RemoteDataType, data: RemoteUploadData) { + if (!isValid()) { + return + } + + val context = weakContext.get() + if (context === null) { + return + } + + val logInfo = "onRemoteRemove(${type.name}, ${data.uuid})" + log("GDriveRemote", logInfo) + when (type) { + RemoteDataType.NOTE -> { + IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) + remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, databaseUpdateLambda) + } + RemoteDataType.NOTE_META -> { + } // Should never happen as note is handling this deletion + RemoteDataType.TAG -> IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) + RemoteDataType.FOLDER -> IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) + RemoteDataType.IMAGE -> { + val imageUUID = toImageUUID(data.uuid) + if (imageUUID !== null) { + val imageFile = ApplicationBase.noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + imageFile.delete() + } + } + } + remoteDatabaseController.remoteDatabaseUpdate(type, data.uuid, databaseUpdateLambda) + } + +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt similarity index 76% rename from scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt rename to scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt index f1b84016..4f94969f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabaseState.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt @@ -1,10 +1,9 @@ -package com.bijoysingh.quicknote.drive +package com.bijoysingh.quicknote.database import android.content.Context -import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.RemoteDatabaseHelper -import com.bijoysingh.quicknote.database.remoteDatabase -import com.bijoysingh.quicknote.database.genRemoteDatabase +import com.bijoysingh.quicknote.drive.ImageUUID +import com.bijoysingh.quicknote.drive.getTrueCurrentTime +import com.bijoysingh.quicknote.drive.toImageUUID import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.database.remote.IRemoteDatabaseState @@ -16,7 +15,7 @@ import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { +class RemoteDatabaseStateController(context: Context) : IRemoteDatabaseState { init { remoteDatabase = genRemoteDatabase(context) @@ -27,8 +26,8 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { */ override fun notifyInsert(data: Any, onExecution: () -> Unit) { when { - data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, onExecution) - data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, onExecution) + data is Tag -> localDatabaseUpdate(RemoteDataType.TAG, data.uuid, onExecution) + data is Folder -> localDatabaseUpdate(RemoteDataType.FOLDER, data.uuid, onExecution) data is Note -> notifyNoteInsertImpl(data, onExecution) else -> maybeThrow("notifyInsert called with unhandled data type") } @@ -42,18 +41,18 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { when { data is Tag -> { - if (database.getByUUID(GDriveDataType.TAG.name, data.uuid) === null) { + if (database.getByUUID(RemoteDataType.TAG.name, data.uuid) === null) { notifyInsert(data) {} } } data is Folder -> { - if (database.getByUUID(GDriveDataType.FOLDER.name, data.uuid) === null) { + if (database.getByUUID(RemoteDataType.FOLDER.name, data.uuid) === null) { notifyInsert(data) {} } } data is Note -> { - if (database.getByUUID(GDriveDataType.NOTE.name, data.uuid) === null - || database.getByUUID(GDriveDataType.NOTE_META.name, data.uuid) === null) { + if (database.getByUUID(RemoteDataType.NOTE.name, data.uuid) === null + || database.getByUUID(RemoteDataType.NOTE_META.name, data.uuid) === null) { notifyInsert(data) {} } } @@ -63,8 +62,8 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { private fun notifyNoteInsertImpl(note: Note, onExecution: () -> Unit) { val noteUuid = note.uuid - localDatabaseUpdate(GDriveDataType.NOTE, noteUuid, onExecution) - localDatabaseUpdate(GDriveDataType.NOTE_META, noteUuid, onExecution) + localDatabaseUpdate(RemoteDataType.NOTE, noteUuid, onExecution) + localDatabaseUpdate(RemoteDataType.NOTE_META, noteUuid, onExecution) val database = remoteDatabase if (database === null) { @@ -75,7 +74,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { val imageUUIDs = HashSet() notifyImageIds(note) { imageUUIDs.add(it) } - database.getByType(GDriveDataType.IMAGE.name) + database.getByType(RemoteDataType.IMAGE.name) .filter { val uuid = toImageUUID(it.uuid) uuid?.noteUuid == note.uuid && !imageUUIDs.contains(uuid) @@ -88,9 +87,9 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { } imageUUIDs.forEach { - val existing = database.getByUUID(GDriveDataType.IMAGE.name, it.name()) + val existing = database.getByUUID(RemoteDataType.IMAGE.name, it.name()) if (existing === null) { - localDatabaseUpdate(GDriveDataType.IMAGE, it.name(), {}, false) + localDatabaseUpdate(RemoteDataType.IMAGE, it.name(), {}, false) } } } @@ -98,11 +97,11 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { override fun notifyRemove(data: Any, onExecution: () -> Unit) { when { - data is Tag -> localDatabaseUpdate(GDriveDataType.TAG, data.uuid, onExecution, true) - data is Folder -> localDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, onExecution, true) + data is Tag -> localDatabaseUpdate(RemoteDataType.TAG, data.uuid, onExecution, true) + data is Folder -> localDatabaseUpdate(RemoteDataType.FOLDER, data.uuid, onExecution, true) data is Note -> { - localDatabaseUpdate(GDriveDataType.NOTE, data.uuid, onExecution, true) - localDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, onExecution, true) + localDatabaseUpdate(RemoteDataType.NOTE, data.uuid, onExecution, true) + localDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, onExecution, true) } else -> maybeThrow("notifyRemove called with unhandled data type") } @@ -114,11 +113,11 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { */ fun stopTrackingItem(data: Any, onExecution: () -> Unit) { when { - data is Tag -> localDatabaseRemove(GDriveDataType.TAG, data.uuid, onExecution) - data is Folder -> localDatabaseRemove(GDriveDataType.FOLDER, data.uuid, onExecution) + data is Tag -> localDatabaseRemove(RemoteDataType.TAG, data.uuid, onExecution) + data is Folder -> localDatabaseRemove(RemoteDataType.FOLDER, data.uuid, onExecution) data is Note -> { - localDatabaseRemove(GDriveDataType.NOTE, data.uuid, onExecution) - localDatabaseRemove(GDriveDataType.NOTE_META, data.uuid, onExecution) + localDatabaseRemove(RemoteDataType.NOTE, data.uuid, onExecution) + localDatabaseRemove(RemoteDataType.NOTE_META, data.uuid, onExecution) } else -> maybeThrow("removeFromDb called with unhandled data type") } @@ -130,7 +129,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { * * @return if false, the item will be deleted from the database */ - fun notifyAttempt(itemType: GDriveDataType, itemUUID: String): Boolean { + fun notifyAttempt(itemType: RemoteDataType, itemUUID: String): Boolean { val database = remoteDatabase if (database === null) { return false @@ -153,7 +152,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { return existing.attempts < 8 } - fun remoteDatabaseUpdate(itemType: GDriveDataType, itemUUID: String, onExecution: () -> Unit) { + fun remoteDatabaseUpdate(itemType: RemoteDataType, itemUUID: String, onExecution: () -> Unit) { GlobalScope.launch { val database = remoteDatabase if (database === null) { @@ -174,7 +173,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { } fun localDatabaseUpdate( - itemType: GDriveDataType, + itemType: RemoteDataType, itemUUID: String, onExecution: () -> Unit, removed: Boolean = false) { @@ -198,7 +197,7 @@ class GDriveRemoteDatabaseState(context: Context) : IRemoteDatabaseState { } fun localDatabaseRemove( - itemType: GDriveDataType, + itemType: RemoteDataType, itemUUID: String, onExecution: () -> Unit) { GlobalScope.launch { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt new file mode 100644 index 00000000..f82d9b35 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt @@ -0,0 +1,14 @@ +package com.bijoysingh.quicknote.database + +interface RemoteFolder { + + fun initContentFolder(resourceId: T?, onSuccess: () -> Unit) + + fun initDeletedFolder(resourceId: T?, onSuccess: () -> Unit) + + fun insert(remoteDataType: RemoteUploadData, resource: D) + + fun delete(remoteDataType: RemoteUploadData) + + fun invalidate() +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt new file mode 100644 index 00000000..1262a1f6 --- /dev/null +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt @@ -0,0 +1,17 @@ +package com.bijoysingh.quicknote.database + +import java.io.File + +open class RemoteResourceId + +interface IRemoteService { + fun readFile(remoteDataType: RemoteUploadData, onRead: (String) -> Unit) + + fun readIntoFile(remoteDataType: RemoteUploadData, file: File, onRead: (Boolean) -> Unit) + + fun createDirectory(parentResourceId: T?, directoryName: String, onSuccess: (T?) -> Unit) + + fun getOrCreateDirectory(parentResourceId: T?, directoryName: String, onSuccess: (T?) -> Unit) + + fun getDirectories(parentResourceId: T, directoryNames: List, onSuccess: (List>) -> Unit) +} diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt index 9bf9337d..f4318663 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt @@ -5,7 +5,7 @@ import com.google.gson.Gson import com.maubis.scarlet.base.support.utils.log import com.maubis.scarlet.base.support.utils.maybeThrow -enum class GDriveDataType { +enum class RemoteDataType { NOTE, NOTE_META, TAG, @@ -57,7 +57,7 @@ class RemoteUploadData { } object RemoteDatabaseHelper { - fun getByUUID(type: GDriveDataType, uuid: String): RemoteUploadData { + fun getByUUID(type: RemoteDataType, uuid: String): RemoteUploadData { return getByUUID(type.name, uuid) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt index 146ff1c6..4debe779 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveAuthenticator.kt @@ -2,7 +2,6 @@ package com.bijoysingh.quicknote.drive import android.content.Context import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 50276882..6edd63b5 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.database.RemoteDatabaseStateController import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.facebook.litho.Component @@ -62,7 +63,7 @@ class GDriveLoginActivity : SecuredActivity(), GoogleApiClient.OnConnectionFaile private fun resetNotesToUploadStateDB() { GlobalScope.launch(Dispatchers.IO) { - val remoteDatabaseState = ApplicationBase.instance.remoteDatabaseState() as GDriveRemoteDatabaseState + val remoteDatabaseState = ApplicationBase.instance.remoteDatabaseState() as RemoteDatabaseStateController ApplicationBase.instance.notesDatabase().getAll().forEach { remoteDatabaseState.notifyInsertIfNotPresent(it) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index db5a9015..f078b91a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -3,7 +3,7 @@ package com.bijoysingh.quicknote.drive import android.app.Dialog import android.graphics.drawable.Drawable import android.text.TextUtils -import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.RemoteDataType import com.bijoysingh.quicknote.database.RemoteUploadData import com.bijoysingh.quicknote.database.remoteDatabase import com.facebook.litho.* @@ -74,19 +74,19 @@ object PendingItemLayoutSpec { val uuid = option.state.uuid val info = option.info ?: "N/A" val icon = when (option.state.type) { - GDriveDataType.NOTE.name -> R.drawable.ic_note_white_48dp - GDriveDataType.NOTE_META.name -> R.drawable.ic_info - GDriveDataType.TAG.name -> R.drawable.ic_action_tags - GDriveDataType.FOLDER.name -> R.drawable.ic_folder - GDriveDataType.IMAGE.name -> R.drawable.ic_image_gallery + RemoteDataType.NOTE.name -> R.drawable.ic_note_white_48dp + RemoteDataType.NOTE_META.name -> R.drawable.ic_info + RemoteDataType.TAG.name -> R.drawable.ic_action_tags + RemoteDataType.FOLDER.name -> R.drawable.ic_folder + RemoteDataType.IMAGE.name -> R.drawable.ic_image_gallery else -> R.drawable.ic_action_lock } val label = when (option.state.type) { - GDriveDataType.NOTE.name -> "Note" - GDriveDataType.NOTE_META.name -> "Info" - GDriveDataType.TAG.name -> "Tag" - GDriveDataType.FOLDER.name -> "Folder" - GDriveDataType.IMAGE.name -> "Image" + RemoteDataType.NOTE.name -> "Note" + RemoteDataType.NOTE_META.name -> "Info" + RemoteDataType.TAG.name -> "Tag" + RemoteDataType.FOLDER.name -> "Folder" + RemoteDataType.IMAGE.name -> "Image" else -> "Invalid" } val localState = when { @@ -223,17 +223,17 @@ class GDrivePendingBottomSheet : LithoBottomSheet() { data.clear() remoteDatabase?.getAllPending()?.forEach { val pendingItem = when (it.type) { - GDriveDataType.NOTE.name -> PendingItem(state = it, info = instance.notesDatabase().getByUUID(it.uuid)?.getFullText()) - GDriveDataType.NOTE_META.name -> { + RemoteDataType.NOTE.name -> PendingItem(state = it, info = instance.notesDatabase().getByUUID(it.uuid)?.getFullText()) + RemoteDataType.NOTE_META.name -> { val note = instance.notesDatabase().getByUUID(it.uuid)?.getExportableNoteMeta() when { (note == null) -> PendingItem(state = it, info = null) else -> PendingItem(state = it, info = Gson().toJson(note)) } } - GDriveDataType.TAG.name -> PendingItem(state = it, info = instance.tagsDatabase().getByUUID(it.uuid)?.title) - GDriveDataType.FOLDER.name -> PendingItem(state = it, info = instance.foldersDatabase().getByUUID(it.uuid)?.title) - GDriveDataType.IMAGE.name -> PendingItem(state = it, info = "Image") + RemoteDataType.TAG.name -> PendingItem(state = it, info = instance.tagsDatabase().getByUUID(it.uuid)?.title) + RemoteDataType.FOLDER.name -> PendingItem(state = it, info = instance.foldersDatabase().getByUUID(it.uuid)?.title) + RemoteDataType.IMAGE.name -> PendingItem(state = it, info = "Image") else -> null } if (pendingItem !== null) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index b785c3e8..f436cf53 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -3,10 +3,7 @@ package com.bijoysingh.quicknote.drive import android.content.Context import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig -import com.bijoysingh.quicknote.database.GDriveDataType -import com.bijoysingh.quicknote.database.RemoteUploadData -import com.bijoysingh.quicknote.database.RemoteUploadDataDao -import com.bijoysingh.quicknote.database.genRemoteDatabase +import com.bijoysingh.quicknote.database.* import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase @@ -80,7 +77,7 @@ fun folderIdForFolderName(folderName: String, folderId: String = ""): String { class GDriveRemoteDatabase(private val weakContext: WeakReference) { - lateinit var gDriveDbState: GDriveRemoteDatabaseState + lateinit var gDriveDbState: RemoteDatabaseStateController private var gDriveDatabase: RemoteUploadDataDao? = null @@ -92,7 +89,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { private var foldersSync: GDriveRemoteFolder? = null private var tagsSync: GDriveRemoteFolder? = null private var imageSync: GDriveRemoteImageFolder? = null - private var syncing = HashMap() + private var syncing = HashMap() private var syncListener: IPendingUploadListener? = null private var databaseUpdateLambda: () -> Unit = { verifyAndNotifyPendingStateChange() } @@ -105,16 +102,16 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { isValidController = true driveHelper = helper gDriveDatabase = genRemoteDatabase(context) - gDriveDbState = Scarlet.gDriveDbState!! + gDriveDbState = Scarlet.remoteDatabaseStateController!! - syncing[GDriveDataType.NOTE] = AtomicBoolean(false) - syncing[GDriveDataType.TAG] = AtomicBoolean(false) - syncing[GDriveDataType.FOLDER] = AtomicBoolean(false) - syncing[GDriveDataType.NOTE_META] = AtomicBoolean(false) - syncing[GDriveDataType.IMAGE] = AtomicBoolean(false) + syncing[RemoteDataType.NOTE] = AtomicBoolean(false) + syncing[RemoteDataType.TAG] = AtomicBoolean(false) + syncing[RemoteDataType.FOLDER] = AtomicBoolean(false) + syncing[RemoteDataType.NOTE_META] = AtomicBoolean(false) + syncing[RemoteDataType.IMAGE] = AtomicBoolean(false) notesSync = GDriveRemoteFolder( - dataType = GDriveDataType.NOTE, + dataType = RemoteDataType.NOTE, database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, @@ -123,7 +120,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() }) notesMetaSync = GDriveRemoteFolder( - dataType = GDriveDataType.NOTE_META, + dataType = RemoteDataType.NOTE_META, database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, @@ -132,7 +129,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() }) tagsSync = GDriveRemoteFolder( - dataType = GDriveDataType.TAG, + dataType = RemoteDataType.TAG, database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, @@ -141,7 +138,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() }) foldersSync = GDriveRemoteFolder( - dataType = GDriveDataType.FOLDER, + dataType = RemoteDataType.FOLDER, database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }, @@ -150,7 +147,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() }) imageSync = GDriveRemoteImageFolder( - dataType = GDriveDataType.IMAGE, + dataType = RemoteDataType.IMAGE, database = gDriveDatabase!!, helper = helper, onPendingChange = { verifyAndNotifyPendingStateChange() }) @@ -208,14 +205,14 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { when (folderName) { FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNote) { - GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE) } + GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE) } sGDriveFirstSyncNote = true } } FOLDER_NAME_NOTES_META -> { notesMetaSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncNoteMeta) { - GlobalScope.launch { resyncDataSync(GDriveDataType.NOTE_META) } + GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE_META) } sGDriveFirstSyncNoteMeta = true } } @@ -223,19 +220,19 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncTag) { - GlobalScope.launch { resyncDataSync(GDriveDataType.TAG) } + GlobalScope.launch { resyncDataSync(RemoteDataType.TAG) } sGDriveFirstSyncTag = true } } FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncFolder) { - GlobalScope.launch { resyncDataSync(GDriveDataType.FOLDER) } + GlobalScope.launch { resyncDataSync(RemoteDataType.FOLDER) } sGDriveFirstSyncFolder = true } } FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { if (!sGDriveFirstSyncImage) { - GlobalScope.launch { resyncDataSync(GDriveDataType.IMAGE) } + GlobalScope.launch { resyncDataSync(RemoteDataType.IMAGE) } sGDriveFirstSyncImage = true } } @@ -355,15 +352,15 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } GlobalScope.launch { - resyncDataSync(GDriveDataType.NOTE) - resyncDataSync(GDriveDataType.NOTE_META) - resyncDataSync(GDriveDataType.TAG) - resyncDataSync(GDriveDataType.FOLDER) - resyncDataSync(GDriveDataType.IMAGE) + resyncDataSync(RemoteDataType.NOTE) + resyncDataSync(RemoteDataType.NOTE_META) + resyncDataSync(RemoteDataType.TAG) + resyncDataSync(RemoteDataType.FOLDER) + resyncDataSync(RemoteDataType.IMAGE) } } - fun resyncDataSync(type: GDriveDataType) { + fun resyncDataSync(type: RemoteDataType) { if (syncing[type]?.getAndSet(true) == true) { return } @@ -400,34 +397,34 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { */ @Suppress("IMPLICIT_CAST_TO_ANY") - private fun insert(type: GDriveDataType, data: RemoteUploadData) { + private fun insert(type: RemoteDataType, data: RemoteUploadData) { if (!isValidController) { return } val logInfo = "insert(${type.name}, ${data.uuid})" val localItem = when (type) { - GDriveDataType.NOTE -> { + RemoteDataType.NOTE -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { notesSync?.insert(data.uuid, this) } } - GDriveDataType.NOTE_META -> { + RemoteDataType.NOTE_META -> { ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { notesMetaSync?.insert(data.uuid, this) } } - GDriveDataType.TAG -> { + RemoteDataType.TAG -> { ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { tagsSync?.insert(this.uuid, this) } } - GDriveDataType.FOLDER -> { + RemoteDataType.FOLDER -> { ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { foldersSync?.insert(this.uuid, this) } } - GDriveDataType.IMAGE -> { + RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(data.uuid) if (imageUUID !== null && noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid).exists()) { imageSync?.insert(imageUUID) @@ -443,7 +440,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } - private fun remove(type: GDriveDataType, data: RemoteUploadData) { + private fun remove(type: RemoteDataType, data: RemoteUploadData) { if (!isValidController) { return } @@ -452,11 +449,11 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { log("GDriveRemote", logInfo) val uuid = data.uuid when (type) { - GDriveDataType.NOTE -> notesSync?.delete(uuid) - GDriveDataType.NOTE_META -> notesMetaSync?.delete(uuid) - GDriveDataType.TAG -> tagsSync?.delete(uuid) - GDriveDataType.FOLDER -> foldersSync?.delete(uuid) - GDriveDataType.IMAGE -> { + RemoteDataType.NOTE -> notesSync?.delete(uuid) + RemoteDataType.NOTE_META -> notesMetaSync?.delete(uuid) + RemoteDataType.TAG -> tagsSync?.delete(uuid) + RemoteDataType.FOLDER -> foldersSync?.delete(uuid) + RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(uuid) when { imageUUID !== null -> imageSync?.delete(imageUUID) @@ -465,7 +462,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } - private fun onRemoteInsert(type: GDriveDataType, data: RemoteUploadData) { + private fun onRemoteInsert(type: RemoteDataType, data: RemoteUploadData) { if (!isValidController) { return } @@ -476,7 +473,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } when (type) { - GDriveDataType.NOTE -> { + RemoteDataType.NOTE -> { onRemoteInsertImpl(data.fileId) { // TODO: De-duplicate meta data and note update try { @@ -487,13 +484,13 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { temporaryNote.description = itemDescription IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.NOTE, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } } } - GDriveDataType.NOTE_META -> { + RemoteDataType.NOTE_META -> { // TODO: De-duplicate meta data and note update onRemoteInsertImpl(data.fileId) { try { @@ -505,46 +502,46 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { temporaryNote.mergeMetas(item) IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } } } - GDriveDataType.TAG -> { + RemoteDataType.TAG -> { onRemoteInsertImpl(data.fileId) { try { val item = Gson().fromJson(it, ExportableTag::class.java) IRemoteDatabaseUtils.onRemoteInsert(context, item) - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.TAG, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.TAG, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } } } - GDriveDataType.FOLDER -> { + RemoteDataType.FOLDER -> { onRemoteInsertImpl(data.fileId) { try { val item = Gson().fromJson(it, ExportableFolder::class.java) IRemoteDatabaseUtils.onRemoteInsert(context, item) - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.FOLDER, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.FOLDER, data.uuid, databaseUpdateLambda) } catch (exception: Exception) { maybeThrow(exception) } } } - GDriveDataType.IMAGE -> { + RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(data.uuid) if (imageUUID !== null) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) if (imageFile.exists()) { - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) return } driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { if (it.result == true) { - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.IMAGE, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) } } } @@ -552,7 +549,7 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { } } - private fun onRemoteRemove(type: GDriveDataType, data: RemoteUploadData) { + private fun onRemoteRemove(type: RemoteDataType, data: RemoteUploadData) { if (!isValidController) { return } @@ -565,15 +562,15 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { val logInfo = "onRemoteRemove(${type.name}, ${data.uuid})" log("GDriveRemote", logInfo) when (type) { - GDriveDataType.NOTE -> { + RemoteDataType.NOTE -> { IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) - gDriveDbState.remoteDatabaseUpdate(GDriveDataType.NOTE_META, data.uuid, databaseUpdateLambda) + gDriveDbState.remoteDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, databaseUpdateLambda) } - GDriveDataType.NOTE_META -> { + RemoteDataType.NOTE_META -> { } // Should never happen as note is handling this deletion - GDriveDataType.TAG -> IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) - GDriveDataType.FOLDER -> IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) - GDriveDataType.IMAGE -> { + RemoteDataType.TAG -> IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) + RemoteDataType.FOLDER -> IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) + RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(data.uuid) if (imageUUID !== null) { val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 63e09e08..1a7afc50 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -1,6 +1,6 @@ package com.bijoysingh.quicknote.drive -import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.RemoteDataType import com.bijoysingh.quicknote.database.RemoteUploadDataDao import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean class GDriveRemoteFolder( - dataType: GDriveDataType, + dataType: RemoteDataType, database: RemoteUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit, diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 012808a9..6cf65dfd 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -1,6 +1,6 @@ package com.bijoysingh.quicknote.drive -import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.RemoteDataType import com.bijoysingh.quicknote.database.RemoteDatabaseHelper import com.bijoysingh.quicknote.database.RemoteUploadDataDao import com.google.api.services.drive.model.File @@ -8,7 +8,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch abstract class GDriveRemoteFolderBase( - val dataType: GDriveDataType, + val dataType: RemoteDataType, val database: RemoteUploadDataDao, val helper: GDriveServiceHelper, val onPendingChange: () -> Unit) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 3ac5ed13..bda16693 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -1,6 +1,6 @@ package com.bijoysingh.quicknote.drive -import com.bijoysingh.quicknote.database.GDriveDataType +import com.bijoysingh.quicknote.database.RemoteDataType import com.bijoysingh.quicknote.database.RemoteUploadDataDao import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import kotlinx.coroutines.Dispatchers @@ -38,7 +38,7 @@ fun toImageUUID(imageUuid: String): ImageUUID? { } class GDriveRemoteImageFolder( - dataType: GDriveDataType, + dataType: RemoteDataType, database: RemoteUploadDataDao, helper: GDriveServiceHelper, onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index be2c91e7..454f1ef8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.database.RemoteDatabaseStateController import com.bijoysingh.quicknote.drive.* import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity @@ -35,7 +36,7 @@ class ScarletAuthenticator() : IAuthenticator { } override fun setup(context: Context) { - Scarlet.gDriveDbState = GDriveRemoteDatabaseState(context) + Scarlet.remoteDatabaseStateController = RemoteDatabaseStateController(context) if (shouldIgnoreFirebase()) { gdrive.setup(context) return diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index badf010d..8915471e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -3,8 +3,7 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context import android.support.v7.app.AppCompatActivity import com.bijoysingh.quicknote.BuildConfig -import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState +import com.bijoysingh.quicknote.Scarlet.Companion.remoteDatabaseStateController import com.bijoysingh.quicknote.firebase.activity.DataPolicyActivity.Companion.openIfNeeded import com.bijoysingh.quicknote.firebase.support.RemoteConfigFetcher import com.maubis.scarlet.base.config.MaterialNoteConfig @@ -31,7 +30,7 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { override fun remoteConfigFetcher(): IRemoteConfigFetcher = RemoteConfigFetcher() - override fun remoteDatabaseState(): IRemoteDatabaseState = gDriveDbState!! + override fun remoteDatabaseState(): IRemoteDatabaseState = remoteDatabaseStateController!! override fun appFlavor(): Flavor = when (BuildConfig.FLAVOR) { "lite" -> Flavor.LITE diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt index fc8bb008..638aabda 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletFolderActor.kt @@ -1,7 +1,7 @@ package com.bijoysingh.quicknote.scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState +import com.bijoysingh.quicknote.Scarlet.Companion.remoteDatabaseStateController import com.maubis.scarlet.base.core.folder.MaterialFolderActor import com.maubis.scarlet.base.database.room.folder.Folder @@ -13,14 +13,14 @@ class ScarletFolderActor(folder: Folder) : MaterialFolderActor(folder) { override fun onlineSave() { super.onlineSave() - gDriveDbState?.notifyInsert(folder, notifyChange) + remoteDatabaseStateController?.notifyInsert(folder, notifyChange) } override fun delete() { super.delete() when { - gDrive?.isValid() == true -> gDriveDbState?.notifyRemove(folder, notifyChange) - else -> gDriveDbState?.stopTrackingItem(folder, notifyChange) + gDrive?.isValid() == true -> remoteDatabaseStateController?.notifyRemove(folder, notifyChange) + else -> remoteDatabaseStateController?.stopTrackingItem(folder, notifyChange) } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt index 784ccff9..828b7ccd 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletNoteActor.kt @@ -2,7 +2,7 @@ package com.bijoysingh.quicknote.scarlet import android.content.Context import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState +import com.bijoysingh.quicknote.Scarlet.Companion.remoteDatabaseStateController import com.maubis.scarlet.base.core.note.MaterialNoteActor import com.maubis.scarlet.base.database.room.note.Note @@ -14,14 +14,14 @@ class ScarletNoteActor(note: Note) : MaterialNoteActor(note) { override fun onlineSave(context: Context) { super.onlineSave(context) - gDriveDbState?.notifyInsert(note, notifyChange) + remoteDatabaseStateController?.notifyInsert(note, notifyChange) } override fun onlineDelete(context: Context) { super.onlineDelete(context) when { - gDrive?.isValid() == true -> gDriveDbState?.notifyRemove(note, notifyChange) - else -> gDriveDbState?.stopTrackingItem(note, notifyChange) + gDrive?.isValid() == true -> remoteDatabaseStateController?.notifyRemove(note, notifyChange) + else -> remoteDatabaseStateController?.stopTrackingItem(note, notifyChange) } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt index 5b36da2f..2c970688 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletTagActor.kt @@ -1,8 +1,7 @@ package com.bijoysingh.quicknote.scarlet -import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveDbState +import com.bijoysingh.quicknote.Scarlet.Companion.remoteDatabaseStateController import com.maubis.scarlet.base.core.tag.MaterialTagActor import com.maubis.scarlet.base.database.room.tag.Tag @@ -14,14 +13,14 @@ class ScarletTagActor(tag: Tag) : MaterialTagActor(tag) { override fun onlineSave() { super.onlineSave() - gDriveDbState?.notifyInsert(tag, notifyChange) + remoteDatabaseStateController?.notifyInsert(tag, notifyChange) } override fun delete() { super.delete() when { - gDrive?.isValid() == true -> gDriveDbState?.notifyRemove(tag, notifyChange) - else -> gDriveDbState?.stopTrackingItem(tag, notifyChange) + gDrive?.isValid() == true -> remoteDatabaseStateController?.notifyRemove(tag, notifyChange) + else -> remoteDatabaseStateController?.stopTrackingItem(tag, notifyChange) } } } \ No newline at end of file From fed30d53ed98e63421120176320046b622aa1b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Thu, 3 Oct 2019 22:35:18 +0200 Subject: [PATCH 087/134] Add Hungarian translation --- base/src/main/res/values-hu/strings.xml | 485 ++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 base/src/main/res/values-hu/strings.xml diff --git a/base/src/main/res/values-hu/strings.xml b/base/src/main/res/values-hu/strings.xml new file mode 100644 index 00000000..151094d6 --- /dev/null +++ b/base/src/main/res/values-hu/strings.xml @@ -0,0 +1,485 @@ + + Maubis Apps + dd month yyyy, hh:mm aa + Cím + Tartalom + Mentés + Importálás + Jegyzetek keresése… + Keresés ezzel… + + Nincsenek jegyzetek + Úgy néz ki, hogy még nem adott hozzá jegyzeteket. Koppintson egy jegyzet hozzáadásához. + + Jegyzet hozzáadása + Ellenőrzőlista hozzáadása + + Jegyzet szerkesztése + Értesítés létrehozása + Megnyitás felugró ablakban + Végleges törlés + Jegyzet másolása + Jegyzet küldése + Jegyzet archiválásának visszavonása + Jegyzet archiválása + Kedvencnek jelölés visszavonása + Kedvencnek jelölés + Jegyzet visszaállítása + Kukába helyezés + Szín módosítása + Jegyzet zárolása + Címkék módosítása + Jegyzet zárolása + Emlékeztető + Kiválasztás + Kétszerezés + Jegyzet rögzítése + Jegyzet rögzítésének feloldása + Zavarásmentés + Jegyzetek egyesítése + Művelet kiválasztása… + + Koppintson a jegyzet szerkesztésre megnyitásához + Koppintson a jegyzet tartalmának vágólapra másolásához + Koppintson a jegyzet tartalmának más alkalmazásokkal megosztásához + Koppintson a jegyzet felugró ablakban megnyitásához + Koppintson a jegyzet törléséhez + Koppintson a jegyzet kukába dobásához + Koppintson a jegyzet helyreállításához a kukából + Koppintson a jegyzet kedvencként megjelöléséhez + Koppintson a jegyzet eltávolításához a kedvencekből + Koppintson a jegyzet archiválásához + Koppintson a jegyzet háttérszínének módosításához + Koppintson a jegyzet helyreállításához az archívumból + + Kezdőlap + Kedvencek + Archivált + Zárolt + Kuka + + Tartalom hozzáadása… + Címsor hozzáadása… + Idézett szöveg hozzáadása… + Elem hozzáadása… + Kód hozzáadása… + Koppintson egy kép hozzáadásához vagy cseréjéhez + + Beállítások + Felület és felhasználói élmény + Válassza ki, hogyan működjön az alkalmazás. + Névjegy + Tudjon meg többet rólunk és az alkalmazásról. + Biztonság + Jegyzetek zárolása és biztonsági beállítások + Listaelrendezés engedélyezése + Jegyzetek megjelenítése egyetlen oszlopban + Rácsnézet engedélyezése + Jegyzetek megjelenítése lépcsőzetes rácsként + Jegyzetek rendezése + Biztonsági mentés és importálás + Biztonsági mentési, importálási és exportálási beállítások + Jegyzetek exportálása + Jegyzetek exportálása az eszköz tárhelyére megosztás céljából + Jegyzetek importálása + Jegyzetek importálása az eszköz tárhelyéről + Exportálás markdownként + Jegyzetek exportálása markdown formátumban. (Ezeket nem importálhatja vissza az alkalmazásba) + Automatikus exportálás + Jegyzetek gyakori exportálása egy külső fájlba biztonsági mentésként + Rólunk + Tudjon meg többet az alkalmazásról és a fejlesztőkről + Nyílt forráskódú projekt + Tudjon meg többet a nyílt forráskódú projektről + Értékelés + Mondja el, mennyire tetszett az alkalmazás + Pro alkalmazás telepítése + A Pro alkalmazás telepítése funkciók feloldásához és a fejlesztő támogatásához + Jegyzetek migrálása a Scarlet Próba + Migrálja az összes jegyzetét a pro alkalmazásba + Jegyzetek törlése és egyebek + Jegyzetek, címkék és egyéb adatok törlése az alkalmazásból + Minden jegyzet törlése + Az összes jegyzet törlése az alkalmazásból + Minden mappa törlése + Az összes mappa törlése az alkalmazásból + Minden címke törlése + Az összes címke törlése az alkalmazásból + Minden törlése + Az összes jegyzet, címke és mappa törlése az alkalmazásból + + Jegyzetmegjelenítő háttere + Témaszín használata a háttérhez + Jegyzetszín használata a háttérhez + + Biztonsági beállítások + Jelkód + Állítson be egy 4 számjegyes PIN-kódot a jegyzetek zárolásához + Enable App Lock + Use PIN to unlock the whole application + Always ask for PIN + Enter PIN for all notes, even if previously entered + Feloldás ujjlenyomattal + A jegyzetek ujjlenyomat megadására kerülnek feloldásra + Ujjlenyomat letiltva + A jegyzetek nem használnak ujjlenyomatot + + Adja meg a PIN-kódot + A feloldáshoz adja meg a PIN-kódot + Adja meg a jelenlegi PIN-kódot + Ellenőrzés + Zárolás + Beállítás + Eltávolítás + + Alapértelmezett jegyzetszín + Válassza ki a jegyzetek alapértelmezett színét + Jegyzetek sorainak számának korlátja + %d sor a kezdőképernyőn + + Markdown támogatás + Markdown támogatott formázás engedélyezése + Markdown a jegyzetlistában + Markdown formázás engedélyezése a jegyzetek listájában + # Heading \n## Subheading \n\n```\nblock of code\n```\n\n> quote\n\n- **bold**\n- *italics*\n- _underline_\n- ~~strike~~\n- `code`\n + + Engedélyek megadása + Az importálás és exportálás tároló engedélyt igényel. Adja meg az engedélyt, ha kérésre kerül. + Engedélyezés + + Biztonsági mentés importálása fájlból + Fájl importálása + Fájlba exportálva + Exportálás sikertelen + Fájlba exportálás… + Kész + Megosztás + Engedélyezés + Letiltás + Mappaszinkronizálás engedélyezése + Mappaszinkronizálás + Szinkronizálja az összes jegyzetét, címkéjét és mappáját egy külső mappába. Ez segít, ha más alkalmazásokkal szinkronizál az eszközei közt, valamint másolatot készít, ha törölnie kell az alkalmazást. + Mappába exportálás + Az összes jegyzet, címke és egyebek szinkronizálása egy külső mappába + MaterialNotes/BACKUP.txt + Zárolt jegyzetek exportálása + Biztonsági másolat készítése a zárolt jegyzetekről is + + %s kB + %s MB + %s GB + + Értékelés a Play Áruházban + Közreműködés + Alkalmazásverzió + Az alkalmazás névjegye + + Programkönyvtárak + Nyílt forráskódú projekt + + Emlékeztető dátuma + Emlékeztető ideje + Ismétlési gyakoriság + Csak egyszer + Naponta + + Jegyzetértesítések + Értesítések és riasztások + + + A %s nyílt forráskódú, és bárki közreműködhet a jobbá tételében. Jelenleg %s készíti és tartja karban. + + + A %s egy egyszerű jegyzetkészítő alkalmazás. Gyors gazdag szöveges bemenetet tesz lehetővé, anélkül hogy nehezen használható volna. Gyerekjátékká teszi a párhuzamos munkavégzést. + + Üdv, egy dizájner és programozó páros vagyunk, akik a %s alkalmazást készítették. + Arra vállalkoztunk, hogy szép, gondosan tervezett, reklámmentes vagy csak minimális reklámokat tartalmazó alkalmazásokat készítsünk, amelyek mindeki számára hasznosak. + + Új címke hozzáadása… + Címke szerkesztése… + címke megadása + + Új címke létrehozása + Címke kiválasztása… + + Jegyzetek rendezése + Legújabb elől + Note Color + Note Tags + Legrégebbi elől + Legutóbb módosítva + Betűrendben + + + Jegyzet kiválasztása + Jegyzetek kiválasztása + Beállítások + A jegyzet törölt vagy zárolt + + + + Újdonságok + Tudja meg, hogy mik az újdonságok az alkalmazás legújabb frissítéseiben + Fordítás + + Sign In with Drive. More Privacy. + Now Photos are Synced too. + App Lock and Single Unlock. + More Languages Supported. + More Widget Settings. + Easier Note Selection. + + Faster Updates and Requests. + App Lock and Single Unlock. + More App Themes. + Note Font Size Selection. + Viewer Background Color. + More Widget Options. + + Másolás + Szövegblokk műveletek + Galéria + Kamera + + Jegyzet felolvasása hangosan + Jegyzet felolvasása + Az elem törlése az összes eszközről törli a képet. + A kép nincs ezen az eszközön + A kép nem tölthető be + A képek nincsenek szinkronizálva + A képek nincsenek szinkronizálva az eszközök között. A képek nem fognak megjelenni a többi eszközön, amikor megnézni a jegyzetet. + Megértettem + + + + Jegyzetek betűmérete + A betűméret módosítása a jegyzetek lapon. Az előnézetben láthatja, hogyan nézne ki. + %d képpontos betűk a jegyzetmegjelenítőben + + + + Telepítés az Áruházból + Alkalmazás telepítése a Google áruházból a felhős szinkronizáció miatt + + + + Nincs PIN-kód megadva + Nem állított be PIN-kódot. Szeretné most beállítani? + Később + Ne kérdezze többet + Beállítás + + + + Biztos benne? + Véglegesen törli a jegyzeteket a kuka mappából? + Véglegesen törli ezt a jegyzetet? + Would you like to permanently delete these notes? + Törlés + Mégse + A jegyzetek 7 nap után véglegesen törlésre kerülnek + + + + Információk + Koppintson a legfrissebb alkalmazásverzió telepítéséhez + Koppintson a Scarlet Próra történő frissítéshez + + + + Bejelentkezés az alkalmazásba + Jelentkezzen be felhős biztonsági mentéshez és szinkronizáláshoz + Kijelentkezés + Kijelentkezés a felhős biztonsági mentés leállításához + Google bejelentkezés sikertelen + + Adatvédelmi irányelvek + Az alkalmazás adatvédelmi irányelvei a tartalomhoz + + Pro alkalmazás telepítése + A Scarlet Próban elérhető + + Miért telepítse a Pro verziót + + ✔ Támogassa a fejlesztőt a felhős szinkronizálás jelentős kiszolgálóköltségeiben \n\n✔ Kapja meg először a legfrissebb funkciókat \n\n✔ Egyes további funkciók csak a Pro felhasználók számára lesz elérhetők + + + + Forget Me + Logout and remove all online data + + + Scarlet + + + + Alkalmazástéma + Válassza ki a téma háttérszínét + Alkalmazástéma kiválasztása + + + Koppintson a címkék hozzáadásához vagy módosításához + + Jegyzetek kiválasztása + Válasson ki több jegyzetet egyszerre, és végezzen rajtuk műveletet + + A jegyzet át lett helyezve a kukába + A jegyzet törlésre került + Visszavonás + + Biztonsági mentés kikapcsolása + Biztonsági mentés bekapcsolása + + Üres jegyzetfüzet + 1 jegyzet + %d jegyzet + Új jegyzetfüzet hozzáadása + Jegyzetfüzet szerkesztése + Jegyzetfüzet hozzáadása + + Jegyzetfüzet hozzáadása + Jegyzetfózet módosítása + + Friss jegyzetek + + + + Szerkesztőbeállítások + A jegyzetszerkesztőt beállításainak és használatának módosítása + + Markdown lehetőséget alapértelmezetten + A markdown gyorsgombok megjelenítése az eszköztáron alapértelmezetten + + Valós idejű markdown + Akkor válassza, ha gépelés közben szeretné látni a markdownt. Bekapcsolása befolyásolhatja a teljesítményt a nagy jegyzeteknél. + + Bejelölt elem áthelyezése + A bejelölt elemek a lista végére kerülnek. A jelölés eltávolítása nem állítja vissza a pozíciót. + + Mozgatási kezelőszervek megjelenítése + Fogantyú megjelenítése az elemek fel és le mozgatásához. A sarkoknál megérintéssel továbbra is átrendezheti a dolgokat. + + Markdown példák + + + + Felületi elem beállítások + A kezdőképernyő felületi elemeinek beállításainak módosítása + + Formázás engedélyezése + A jegyzetek formázva jelennek meg a kezdőképernyőn + + Zárolt jegyzetek megjelenítése + A zárolt jegyzetek megjelenhetnek a kezdőképernyőn + + Archivált jegyzetek megjelenítése + Az archivált jegyzetek megjelenhetnek a kezdőképernyőn + + Kukában lévő jegyzetek megjelenítése + A kukában lévő jegyzetek megjelenhetnek a kezdőképernyőn + + Widget Background + Choose the background color for the multi-notes widget + + Show Action Toolbar + Multi-notes widget shows the toolbar + + + + + Súgó és gyakori kérdések + Kapjon segítséget az alkalmazás funkcióinak használatához + + Szinkronizálás + Biztonsági mentés folyamatban + Local + Remote + + Unavailable + Created + Deleted + Updated + + + Fejlesztői beállítások + Az alkalmazás belső beállításainak módosítása hibakeresés céljából + + Kivételek naplózása + Elkapott kivételek naplózása egy rögzített bejegyzésbe, hogy továbbküldhesse a fejlesztőnek + + Kivételek megjelenítése egy lapon + Elkapott kivételek megjelenítése egy lapon, ha az lehetséges, hogy továbbküldhesse a fejlesztőnek + + Kivételek továbbdobása + Kivételek továbbdobása és az alkalmazás összeomlasztása. 5 összeomlás után vissza lesz állítva + + Teljes képernyő engedélyezése + Az alkalmazás teljes képernyősre állítása, hogy képernyőképeket és -felvételeket készíthessen + + Jegyzetek UUID azonosítójának megjelenítése + A jegyzetek egyedi azonosítójának megjelenítése a kezdőképernyőn + + Hamis kivételek + Hamis kivétel dobása a kivételfunkciók teszteléséhez + + Kivétel dobva + Alkalmazás összeomlasztása + E-mail + + + Kapcsolódás sikertelen + Szinkronizálás a Google Drive-on + A Google Drive-val szinkronizálhatja a jegyzeteit az eszközei között a saját Google Drive fiókjával. + Már bejelentkezett a Google Firebase-be? + Bejelentkezés a Google Drive-ba + Bejelentkezés… + + Kijelentkezés a Scarletből + Jegyzetek, címkék és mappák szinkronizálásának leállítása. + Jegyzetek, címkék és mappák Google Drive szinkronizálásának leállítása. + Az adatai megmaradnak az alkalmazásban és a Google Drive-on. + Kijelentkezés a Drive-ból + Kijelentkezés… + + A jegyzetei, címkéi és mappái a saját Google Drive-jában lesznek tárolva, így a hozzáférésük felett Ön rendelkezik. + A fényképei is fel lesznek töltve, és szinkronizálásra kerülnek a többi eszközével. + + + Adatok visszaállítása a Firebase-ből + Google bejelentkezés + Bejelentkezés… + + Az adatait a Google Firebase-ben tároltuk. Bejelentkezés után visszaállítjuk ezeket az eszközére + Az új jegyzetek NEM lesznek szinkronizálva a Google Firebase-be, és be kell jelentkeznie a Google Drive-ba + Ha helyreállításra kerültek a jegyzetei, törölheti azokat a Google Firebase-ből, és átválthat + + Az adatai nem szinkronizáltak. + A régi bejelentkezésre épülő adatszinkronizálás letiltásra került, fontolja meg, hogy a Google Drive-alapú szinkronizálásra vált. + + Adatok átvitele a Firebase-ből a Drive-ba + Adatok törlése és kijelentkezés + Törlés és kijelentkezés + Az adatait eddig a Google Firebase-ben tároltuk. A jegyzetei tárolását átvisszük a Google Drive-jába. + Több adatvédelem. + Fényképszinkronizálás. + Következő lépések + Először töröljük a jegyzeteit a Firebase-ből és kijelentkeztetjük. + Aztán bejelentkezhet a Google Drive-ba, és feltöltjük az adatait a képeivel együtt. + + + + Unlock App + Enter 4 digit PIN or use fingerprint to unlock + Enter 4 digit PIN to unlock + + + + + + + + Use System Theme + Use the light or dark theme from the system + + + From 680d7936899281fdddd8ec0c236bfe6ec1135b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Tue, 8 Oct 2019 08:15:21 +0200 Subject: [PATCH 088/134] Removing untranslatable lines, spellcheck --- base/src/main/res/values-hu/strings.xml | 78 ++----------------------- 1 file changed, 4 insertions(+), 74 deletions(-) diff --git a/base/src/main/res/values-hu/strings.xml b/base/src/main/res/values-hu/strings.xml index 151094d6..03aca42e 100644 --- a/base/src/main/res/values-hu/strings.xml +++ b/base/src/main/res/values-hu/strings.xml @@ -1,6 +1,4 @@ - Maubis Apps - dd month yyyy, hh:mm aa Cím Tartalom Mentés @@ -115,10 +113,6 @@ Biztonsági beállítások Jelkód Állítson be egy 4 számjegyes PIN-kódot a jegyzetek zárolásához - Enable App Lock - Use PIN to unlock the whole application - Always ask for PIN - Enter PIN for all notes, even if previously entered Feloldás ujjlenyomattal A jegyzetek ujjlenyomat megadására kerülnek feloldásra Ujjlenyomat letiltva @@ -141,7 +135,6 @@ Markdown támogatott formázás engedélyezése Markdown a jegyzetlistában Markdown formázás engedélyezése a jegyzetek listájában - # Heading \n## Subheading \n\n```\nblock of code\n```\n\n> quote\n\n- **bold**\n- *italics*\n- _underline_\n- ~~strike~~\n- `code`\n Engedélyek megadása Az importálás és exportálás tároló engedélyt igényel. Adja meg az engedélyt, ha kérésre kerül. @@ -161,13 +154,7 @@ Szinkronizálja az összes jegyzetét, címkéjét és mappáját egy külső mappába. Ez segít, ha más alkalmazásokkal szinkronizál az eszközei közt, valamint másolatot készít, ha törölnie kell az alkalmazást. Mappába exportálás Az összes jegyzet, címke és egyebek szinkronizálása egy külső mappába - MaterialNotes/BACKUP.txt Zárolt jegyzetek exportálása - Biztonsági másolat készítése a zárolt jegyzetekről is - - %s kB - %s MB - %s GB Értékelés a Play Áruházban Közreműködés @@ -193,7 +180,7 @@ A %s egy egyszerű jegyzetkészítő alkalmazás. Gyors gazdag szöveges bemenetet tesz lehetővé, anélkül hogy nehezen használható volna. Gyerekjátékká teszi a párhuzamos munkavégzést. Üdv, egy dizájner és programozó páros vagyunk, akik a %s alkalmazást készítették. - Arra vállalkoztunk, hogy szép, gondosan tervezett, reklámmentes vagy csak minimális reklámokat tartalmazó alkalmazásokat készítsünk, amelyek mindeki számára hasznosak. + Arra vállalkoztunk, hogy szép, gondosan tervezett, reklámmentes vagy csak minimális reklámokat tartalmazó alkalmazásokat készítsünk, amelyek mindenki számára hasznosak. Új címke hozzáadása… Címke szerkesztése… @@ -204,8 +191,6 @@ Jegyzetek rendezése Legújabb elől - Note Color - Note Tags Legrégebbi elől Legutóbb módosítva Betűrendben @@ -221,21 +206,6 @@ Újdonságok Tudja meg, hogy mik az újdonságok az alkalmazás legújabb frissítéseiben Fordítás - - Sign In with Drive. More Privacy. - Now Photos are Synced too. - App Lock and Single Unlock. - More Languages Supported. - More Widget Settings. - Easier Note Selection. - - Faster Updates and Requests. - App Lock and Single Unlock. - More App Themes. - Note Font Size Selection. - Viewer Background Color. - More Widget Options. - Másolás Szövegblokk műveletek Galéria @@ -274,7 +244,6 @@ Biztos benne? Véglegesen törli a jegyzeteket a kuka mappából? Véglegesen törli ezt a jegyzetet? - Would you like to permanently delete these notes? Törlés Mégse A jegyzetek 7 nap után véglegesen törlésre kerülnek @@ -305,13 +274,6 @@ - Forget Me - Logout and remove all online data - - - Scarlet - - Alkalmazástéma Válassza ki a téma háttérszínét @@ -321,7 +283,7 @@ Koppintson a címkék hozzáadásához vagy módosításához Jegyzetek kiválasztása - Válasson ki több jegyzetet egyszerre, és végezzen rajtuk műveletet + Választáson ki több jegyzetet egyszerre, és végezzen rajtuk műveletet A jegyzet át lett helyezve a kukába A jegyzet törlésre került @@ -338,11 +300,10 @@ Jegyzetfüzet hozzáadása Jegyzetfüzet hozzáadása - Jegyzetfózet módosítása + Jegyzetfőzet módosítása Friss jegyzetek - Szerkesztőbeállítások A jegyzetszerkesztőt beállításainak és használatának módosítása @@ -377,14 +338,6 @@ Kukában lévő jegyzetek megjelenítése A kukában lévő jegyzetek megjelenhetnek a kezdőképernyőn - - Widget Background - Choose the background color for the multi-notes widget - - Show Action Toolbar - Multi-notes widget shows the toolbar - - Súgó és gyakori kérdések @@ -392,13 +345,6 @@ Szinkronizálás Biztonsági mentés folyamatban - Local - Remote - - Unavailable - Created - Deleted - Updated Fejlesztői beállítások @@ -461,25 +407,9 @@ Törlés és kijelentkezés Az adatait eddig a Google Firebase-ben tároltuk. A jegyzetei tárolását átvisszük a Google Drive-jába. Több adatvédelem. - Fényképszinkronizálás. + Fénykép-szinkronizálás Következő lépések Először töröljük a jegyzeteit a Firebase-ből és kijelentkeztetjük. Aztán bejelentkezhet a Google Drive-ba, és feltöltjük az adatait a képeivel együtt. - - - Unlock App - Enter 4 digit PIN or use fingerprint to unlock - Enter 4 digit PIN to unlock - - - - - - - - Use System Theme - Use the light or dark theme from the system - - From fd534a0d5931ed2ece5fe8ee1c98c7924ebe0cce Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 16 Oct 2019 00:02:45 +0100 Subject: [PATCH 089/134] [WebDAV][3] Abstracting Google Drive completely out (Some fixes pending) --- .../java/com/bijoysingh/quicknote/Scarlet.kt | 14 +- .../quicknote/database/RemoteController.kt | 142 +++-- .../quicknote/database/RemoteFolder.kt | 6 +- .../quicknote/database/RemoteService.kt | 21 +- .../quicknote/drive/GDriveLoginActivity.kt | 9 +- .../quicknote/drive/GDriveRemoteDatabase.kt | 587 ++---------------- .../quicknote/drive/GDriveRemoteFolder.kt | 161 +++-- .../quicknote/drive/GDriveRemoteFolderBase.kt | 10 +- .../drive/GDriveRemoteImageFolder.kt | 95 ++- .../quicknote/drive/GDriveServiceHelper.kt | 215 ++++--- .../quicknote/scarlet/ScarletAuthenticator.kt | 8 +- 11 files changed, 453 insertions(+), 815 deletions(-) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index dcb1f51b..c32d8c6b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -13,20 +13,20 @@ class Scarlet : ApplicationBase() { override fun onCreate() { super.onCreate() - gDriveConfig = Store.get(this, "gdrive_config") - ApplicationBase.instance = ScarletConfig(this) + remoteConfig = Store.get(this, "gdrive_config") - ApplicationBase.instance.themeController().setup(this) - ApplicationBase.instance.authenticator().setup(this) - ApplicationBase.instance.remoteConfigFetcher().setup(this) + instance = ScarletConfig(this) + instance.themeController().setup(this) + instance.authenticator().setup(this) + instance.remoteConfigFetcher().setup(this) ExternalFolderSync.setup(this) } companion object { var firebase: FirebaseRemoteDatabase? = null var gDrive: GDriveRemoteDatabase? = null - var gDriveConfig: Store? = null - var remoteDatabaseStateController: RemoteDatabaseStateController? = null + + lateinit var remoteConfig: Store } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt index 2228540b..9c4ac24e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt @@ -1,8 +1,11 @@ package com.bijoysingh.quicknote.database import android.content.Context -import com.bijoysingh.quicknote.Scarlet -import com.bijoysingh.quicknote.drive.* +import com.bijoysingh.quicknote.Scarlet.Companion.remoteConfig +import com.bijoysingh.quicknote.drive.GOOGLE_DRIVE_ROOT_FOLDER +import com.bijoysingh.quicknote.drive.getTrueCurrentTime +import com.bijoysingh.quicknote.drive.sSyncingCount +import com.bijoysingh.quicknote.drive.toImageUUID import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase @@ -21,25 +24,61 @@ import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean -abstract class RemoteController(private val weakContext: WeakReference) { + +const val FOLDER_NAME_IMAGES = "images" +const val FOLDER_NAME_NOTES = "notes" +const val FOLDER_NAME_NOTES_META = "notes_meta" +const val FOLDER_NAME_TAGS = "tags" +const val FOLDER_NAME_FOLDERS = "folders" +const val FOLDER_NAME_DELETED_NOTES = "deleted_notes" +const val FOLDER_NAME_DELETED_TAGS = "deleted_tags" +const val FOLDER_NAME_DELETED_FOLDERS = "deleted_folders" + +const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META = "g_drive_first_time_sync_note_meta" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" +const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" +var sRemoteFirstSyncNoteMeta: Boolean + get() = remoteConfig.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META, false) + set(value) = remoteConfig.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META, value) +var sRemoteFirstSyncNote: Boolean + get() = remoteConfig.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) + set(value) = remoteConfig.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) +var sRemoteFirstSyncTag: Boolean + get() = remoteConfig.get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) + set(value) = remoteConfig.put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) +var sRemoteFirstSyncFolder: Boolean + get() = remoteConfig.get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) + set(value) = remoteConfig.put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) +var sRemoteFirstSyncImage: Boolean + get() = remoteConfig.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) + set(value) = remoteConfig.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) + +const val KEY_G_DRIVE_LAST_FULL_SYNC_TIME = "g_drive_last_full_sync_time" +var sRemoteLastFullSyncTime: Long + get() = remoteConfig.get(KEY_G_DRIVE_LAST_FULL_SYNC_TIME, 0L) + set(value) = remoteConfig.put(KEY_G_DRIVE_LAST_FULL_SYNC_TIME, value) + +abstract class RemoteController(private val weakContext: WeakReference) { private val isValidController: AtomicBoolean = AtomicBoolean(true) lateinit var remoteDatabaseController: RemoteDatabaseStateController - lateinit var remoteService: IRemoteService + lateinit var remoteService: IRemoteService lateinit var remoteDatabase: RemoteUploadDataDao - lateinit var notesSync: RemoteFolder - lateinit var notesMetaSync: RemoteFolder - lateinit var foldersSync: RemoteFolder - lateinit var tagsSync: RemoteFolder - lateinit var imageSync: RemoteFolder + lateinit var notesSync: RemoteFolder + lateinit var notesMetaSync: RemoteFolder + lateinit var foldersSync: RemoteFolder + lateinit var tagsSync: RemoteFolder + lateinit var imageSync: RemoteFolder private var syncing = HashMap() private var syncListener: IPendingUploadListener? = null private var databaseUpdateLambda: () -> Unit = { verifyAndNotifyPendingStateChange() } - fun init(service: IRemoteService) { + fun init(service: IRemoteService) { val context = weakContext.get() if (context === null) { return @@ -48,6 +87,7 @@ abstract class RemoteController(private val weakContext: W isValidController.set(true) remoteService = service remoteDatabase = genRemoteDatabase(context)!! + remoteDatabaseController = RemoteDatabaseStateController(context) syncing[RemoteDataType.NOTE] = AtomicBoolean(false) syncing[RemoteDataType.TAG] = AtomicBoolean(false) @@ -59,10 +99,17 @@ abstract class RemoteController(private val weakContext: W initRootFolder() } - private fun isValid(): Boolean { + fun isValid(): Boolean { return isValidController.get() } + fun logout() { + GlobalScope.launch { + reset() + remoteConfig.clearSync() + } + } + fun reset() { isValidController.set(false) @@ -72,22 +119,14 @@ abstract class RemoteController(private val weakContext: W imageSync.invalidate() } - - fun logout() { - GlobalScope.launch { - reset() - Scarlet.gDriveConfig?.clearSync() - } - } - - /** * Abstract Methods */ abstract fun initSyncs() - abstract fun getResourceIdForFolderName(folderName: String): T? - abstract fun storeResourceIdForFolderName(folderName: String, resource: T) + abstract fun getResourceId(data: RemoteUploadData): ResourceId + abstract fun getResourceIdForFolderName(folderName: String): ResourceId? + abstract fun storeResourceIdForFolderName(folderName: String, resource: ResourceId) /** * Initialisation Methods @@ -95,7 +134,7 @@ abstract class RemoteController(private val weakContext: W private fun initRootFolder() { GlobalScope.launch { - sGDriveLastFullSyncTime = getTrueCurrentTime() + sRemoteLastFullSyncTime = getTrueCurrentTime() val fuid = getResourceIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) when { fuid !== null -> onRootFolderLoaded(fuid) @@ -114,12 +153,12 @@ abstract class RemoteController(private val weakContext: W } } - private fun onRootFolderLoaded(rootFolderId: T) { + private fun onRootFolderLoaded(rootFolderId: ResourceId) { createFolders(rootFolderId, listOf(FOLDER_NAME_NOTES, FOLDER_NAME_NOTES_META, FOLDER_NAME_FOLDERS, FOLDER_NAME_TAGS, FOLDER_NAME_IMAGES)) createFolders(rootFolderId, listOf(FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS)) } - private fun createFolders(rootFolderId: T, expectedFolders: List) { + private fun createFolders(rootFolderId: ResourceId, expectedFolders: List) { val knownFolderIds = expectedFolders.filter { getResourceIdForFolderName(it) !== null } knownFolderIds.forEach { GlobalScope.launch { initSubRootFolder(it, getResourceIdForFolderName(it)!!) } @@ -154,39 +193,39 @@ abstract class RemoteController(private val weakContext: W } } - private fun initSubRootFolder(folderName: String, folderId: T) { + private fun initSubRootFolder(folderName: String, folderId: ResourceId) { when (folderName) { FOLDER_NAME_NOTES -> notesSync.initContentFolder(folderId) { - if (!sGDriveFirstSyncNote) { + if (!sRemoteFirstSyncNote) { GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE) } - sGDriveFirstSyncNote = true + sRemoteFirstSyncNote = true } } FOLDER_NAME_NOTES_META -> { notesMetaSync.initContentFolder(folderId) { - if (!sGDriveFirstSyncNoteMeta) { + if (!sRemoteFirstSyncNoteMeta) { GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE_META) } - sGDriveFirstSyncNoteMeta = true + sRemoteFirstSyncNoteMeta = true } } notesMetaSync.initDeletedFolder(null) {} } FOLDER_NAME_TAGS -> tagsSync.initContentFolder(folderId) { - if (!sGDriveFirstSyncTag) { + if (!sRemoteFirstSyncTag) { GlobalScope.launch { resyncDataSync(RemoteDataType.TAG) } - sGDriveFirstSyncTag = true + sRemoteFirstSyncTag = true } } FOLDER_NAME_FOLDERS -> foldersSync.initContentFolder(folderId) { - if (!sGDriveFirstSyncFolder) { + if (!sRemoteFirstSyncFolder) { GlobalScope.launch { resyncDataSync(RemoteDataType.FOLDER) } - sGDriveFirstSyncFolder = true + sRemoteFirstSyncFolder = true } } FOLDER_NAME_IMAGES -> imageSync.initContentFolder(folderId) { - if (!sGDriveFirstSyncImage) { + if (!sRemoteFirstSyncImage) { GlobalScope.launch { resyncDataSync(RemoteDataType.IMAGE) } - sGDriveFirstSyncImage = true + sRemoteFirstSyncImage = true } } FOLDER_NAME_DELETED_NOTES -> notesSync.initDeletedFolder(folderId) { @@ -212,7 +251,16 @@ abstract class RemoteController(private val weakContext: W } } - private fun verifyAndNotifyPendingStateChange() { + fun notifyPendingSyncChange(action: String) { + log("GDrive", "notifyPendingSyncChange($action)") + val count = sSyncingCount.get() + when { + count <= 0 -> syncListener?.onPendingSyncsUpdate(false) + count >= 1 -> syncListener?.onPendingSyncsUpdate(true) + } + } + + fun verifyAndNotifyPendingStateChange() { GlobalScope.launch { val currentPendingState = remoteDatabase.getPendingCount() > 0 val pending = remoteDatabase.getAllPending().map { "type=${it.type}, uuid=${it.uuid}, fid=${it.fileId}" }.joinToString(separator = "\n") @@ -222,14 +270,6 @@ abstract class RemoteController(private val weakContext: W } } - fun notifyPendingSyncChange(action: String) { - val count = sSyncingCount.get() - when { - count <= 0 -> syncListener?.onPendingSyncsUpdate(false) - count >= 1 -> syncListener?.onPendingSyncsUpdate(true) - } - } - /** * Notify local changes to the notes */ @@ -251,7 +291,7 @@ abstract class RemoteController(private val weakContext: W return } - if (forced || (getTrueCurrentTime() - sGDriveLastFullSyncTime > 1000 * 60 * 60 * 24)) { + if (forced || (getTrueCurrentTime() - sRemoteLastFullSyncTime > 1000 * 60 * 60 * 24)) { GlobalScope.launch { remoteDatabase.resetAttempts() initRootFolder() @@ -375,7 +415,7 @@ abstract class RemoteController(private val weakContext: W when (type) { RemoteDataType.NOTE -> { - remoteService.readFile(data) { content -> + remoteService.readFile(getResourceId(data)) { content -> // TODO: De-duplicate meta data and note update try { val itemDescription = fromExportedMarkdown(content) @@ -393,7 +433,7 @@ abstract class RemoteController(private val weakContext: W } RemoteDataType.NOTE_META -> { // TODO: De-duplicate meta data and note update - remoteService.readFile(data) { content -> + remoteService.readFile(getResourceId(data)) { content -> try { val item = Gson().fromJson(content, ExportableNoteMeta::class.java) @@ -410,7 +450,7 @@ abstract class RemoteController(private val weakContext: W } } RemoteDataType.TAG -> { - remoteService.readFile(data) { content -> + remoteService.readFile(getResourceId(data)) { content -> try { val item = Gson().fromJson(content, ExportableTag::class.java) IRemoteDatabaseUtils.onRemoteInsert(context, item) @@ -421,7 +461,7 @@ abstract class RemoteController(private val weakContext: W } } RemoteDataType.FOLDER -> { - remoteService.readFile(data) { content -> + remoteService.readFile(getResourceId(data)) { content -> try { val item = Gson().fromJson(content, ExportableFolder::class.java) IRemoteDatabaseUtils.onRemoteInsert(context, item) @@ -440,7 +480,7 @@ abstract class RemoteController(private val weakContext: W return } - remoteService.readIntoFile(data, imageFile) { success -> + remoteService.readIntoFile(getResourceId(data), imageFile) { success -> if (success) { remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt index f82d9b35..54828fa4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteFolder.kt @@ -1,14 +1,14 @@ package com.bijoysingh.quicknote.database -interface RemoteFolder { +interface RemoteFolder { fun initContentFolder(resourceId: T?, onSuccess: () -> Unit) fun initDeletedFolder(resourceId: T?, onSuccess: () -> Unit) - fun insert(remoteDataType: RemoteUploadData, resource: D) + fun insert(remoteData: RemoteUploadData, resource: D) - fun delete(remoteDataType: RemoteUploadData) + fun delete(remoteData: RemoteUploadData) fun invalidate() } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt index 1262a1f6..2595c394 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteService.kt @@ -2,16 +2,23 @@ package com.bijoysingh.quicknote.database import java.io.File -open class RemoteResourceId +interface IRemoteService { -interface IRemoteService { - fun readFile(remoteDataType: RemoteUploadData, onRead: (String) -> Unit) + fun createDirectory(parentResourceId: ResourceIdType?, directoryName: String, onSuccess: (ResourceIdType?) -> Unit) - fun readIntoFile(remoteDataType: RemoteUploadData, file: File, onRead: (Boolean) -> Unit) + fun getOrCreateDirectory(parentResourceId: ResourceIdType?, directoryName: String, onSuccess: (ResourceIdType?) -> Unit) - fun createDirectory(parentResourceId: T?, directoryName: String, onSuccess: (T?) -> Unit) + fun getDirectories(parentResourceId: ResourceIdType, directoryNames: List, onSuccess: (List>) -> Unit) - fun getOrCreateDirectory(parentResourceId: T?, directoryName: String, onSuccess: (T?) -> Unit) + fun createFileWithData(parentResourceId: ResourceIdType, name: String, content: String, updateTime: Long, onSuccess: (FileType?) -> Unit) + fun createFileFromFile(parentResourceId: ResourceIdType, name: String, localFile: File, updateTime: Long, onSuccess: (FileType?) -> Unit) - fun getDirectories(parentResourceId: T, directoryNames: List, onSuccess: (List>) -> Unit) + fun updateFileWithData(resourceId: ResourceIdType, name: String, content: String, updateTime: Long, onSuccess: (FileType?) -> Unit) + + fun readFile(resourceId: ResourceIdType, onRead: (String) -> Unit) + fun readIntoFile(resourceId: ResourceIdType, destinationFile: File, onRead: (Boolean) -> Unit) + + fun removeFileOrFolder(resourceId: ResourceIdType, onSuccess: (Boolean) -> Unit) + + fun getFilesInFolder(parentResourceId: ResourceIdType, mimeType: String, onSuccess: (FileListType?) -> Unit) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index 6edd63b5..ee90fad4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -6,6 +6,9 @@ import android.os.Bundle import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.RemoteDatabaseStateController +import com.bijoysingh.quicknote.database.sRemoteFirstSyncFolder +import com.bijoysingh.quicknote.database.sRemoteFirstSyncNote +import com.bijoysingh.quicknote.database.sRemoteFirstSyncTag import com.bijoysingh.quicknote.firebase.activity.FirebaseLoginActivity import com.bijoysingh.quicknote.scarlet.sGDriveLoggedIn import com.facebook.litho.Component @@ -144,9 +147,9 @@ class GDriveLoginActivity : SecuredActivity(), GoogleApiClient.OnConnectionFaile fun onLoginComplete(account: GoogleSignInAccount) { mDriveServiceHelper = getDriveHelper(context, account) - sGDriveFirstSyncNote = false - sGDriveFirstSyncFolder = false - sGDriveFirstSyncTag = false + sRemoteFirstSyncNote = false + sRemoteFirstSyncFolder = false + sRemoteFirstSyncTag = false gDrive?.reset() gDrive = GDriveRemoteDatabase(WeakReference(this.applicationContext)) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index f436cf53..76b4b6d1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -1,119 +1,27 @@ package com.bijoysingh.quicknote.drive import android.content.Context -import com.bijoysingh.quicknote.Scarlet -import com.bijoysingh.quicknote.Scarlet.Companion.gDriveConfig -import com.bijoysingh.quicknote.database.* -import com.bijoysingh.quicknote.firebase.data.getFirebaseNote +import com.bijoysingh.quicknote.Scarlet.Companion.remoteConfig +import com.bijoysingh.quicknote.database.RemoteController +import com.bijoysingh.quicknote.database.RemoteDataType +import com.bijoysingh.quicknote.database.RemoteUploadData +import com.google.api.services.drive.model.File +import com.google.api.services.drive.model.FileList import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.config.auth.IPendingUploadListener -import com.maubis.scarlet.base.core.note.NoteBuilder -import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils -import com.maubis.scarlet.base.export.data.* -import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor +import com.maubis.scarlet.base.export.data.getExportableFolder +import com.maubis.scarlet.base.export.data.getExportableNoteMeta +import com.maubis.scarlet.base.export.data.getExportableTag +import com.maubis.scarlet.base.export.data.toExportedMarkdown import com.maubis.scarlet.base.support.utils.log -import com.maubis.scarlet.base.support.utils.maybeThrow -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import java.lang.ref.WeakReference -import java.util.concurrent.atomic.AtomicBoolean - -const val FOLDER_NAME_IMAGES = "images" -const val FOLDER_NAME_NOTES = "notes" -const val FOLDER_NAME_NOTES_META = "notes_meta" -const val FOLDER_NAME_TAGS = "tags" -const val FOLDER_NAME_FOLDERS = "folders" -const val FOLDER_NAME_DELETED_NOTES = "deleted_notes" -const val FOLDER_NAME_DELETED_TAGS = "deleted_tags" -const val FOLDER_NAME_DELETED_FOLDERS = "deleted_folders" - -const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE = "g_drive_first_time_sync_note" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META = "g_drive_first_time_sync_note_meta" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_TAG = "g_drive_first_time_sync_tag" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER = "g_drive_first_time_sync_folder" -const val KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE = "g_drive_first_time_sync_image" -var sGDriveFirstSyncNoteMeta: Boolean - get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META, false) ?: false - set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE_META, value) ?: Unit -var sGDriveFirstSyncNote: Boolean - get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, false) ?: false - set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_NOTE, value) ?: Unit -var sGDriveFirstSyncTag: Boolean - get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, false) ?: false - set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_TAG, value) ?: Unit -var sGDriveFirstSyncFolder: Boolean - get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, false) ?: false - set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_FOLDER, value) ?: Unit -var sGDriveFirstSyncImage: Boolean - get() = gDriveConfig?.get(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, false) ?: false - set(value) = gDriveConfig?.put(KEY_G_DRIVE_FIRST_TIME_SYNC_IMAGE, value) ?: Unit - -const val KEY_G_DRIVE_LAST_FULL_SYNC_TIME = "g_drive_last_full_sync_time" -var sGDriveLastFullSyncTime: Long - get() = gDriveConfig?.get(KEY_G_DRIVE_LAST_FULL_SYNC_TIME, 0L) ?: 0L - set(value) = gDriveConfig?.put(KEY_G_DRIVE_LAST_FULL_SYNC_TIME, value) ?: Unit - -fun folderIdForFolderName(folderName: String, folderId: String = ""): String { - val key = "g_drive_folder_if_for_$folderName" - if (folderId.isEmpty()) { - // Get Condition - var storedValue = gDriveConfig?.get(key, "") ?: "" - if (storedValue == INVALID_FILE_ID) { - gDriveConfig?.put(key, "") - storedValue = "" - } - log("GDrive", "folderIdForFolderName($folderName, $storedValue") - return storedValue - } - - if (folderId != INVALID_FILE_ID) { - gDriveConfig?.put(key, folderId) - } - return folderId -} - -class GDriveRemoteDatabase(private val weakContext: WeakReference) { - - lateinit var gDriveDbState: RemoteDatabaseStateController - - private var gDriveDatabase: RemoteUploadDataDao? = null - - private var isValidController: Boolean = true - private var driveHelper: GDriveServiceHelper? = null - - private var notesSync: GDriveRemoteFolder? = null - private var notesMetaSync: GDriveRemoteFolder? = null - private var foldersSync: GDriveRemoteFolder? = null - private var tagsSync: GDriveRemoteFolder? = null - private var imageSync: GDriveRemoteImageFolder? = null - private var syncing = HashMap() - private var syncListener: IPendingUploadListener? = null - private var databaseUpdateLambda: () -> Unit = { verifyAndNotifyPendingStateChange() } - - fun init(helper: GDriveServiceHelper) { - val context = weakContext.get() - if (context === null) { - return - } - - isValidController = true - driveHelper = helper - gDriveDatabase = genRemoteDatabase(context) - gDriveDbState = Scarlet.remoteDatabaseStateController!! - - syncing[RemoteDataType.NOTE] = AtomicBoolean(false) - syncing[RemoteDataType.TAG] = AtomicBoolean(false) - syncing[RemoteDataType.FOLDER] = AtomicBoolean(false) - syncing[RemoteDataType.NOTE_META] = AtomicBoolean(false) - syncing[RemoteDataType.IMAGE] = AtomicBoolean(false) +class GDriveRemoteDatabase(weakContext: WeakReference) : RemoteController(weakContext) { + override fun initSyncs() { notesSync = GDriveRemoteFolder( dataType = RemoteDataType.NOTE, - database = gDriveDatabase!!, - helper = helper, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { it }, uuidToObject = { @@ -121,8 +29,8 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { }) notesMetaSync = GDriveRemoteFolder( dataType = RemoteDataType.NOTE_META, - database = gDriveDatabase!!, - helper = helper, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { Gson().toJson(it) }, uuidToObject = { @@ -130,8 +38,8 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { }) tagsSync = GDriveRemoteFolder( dataType = RemoteDataType.TAG, - database = gDriveDatabase!!, - helper = helper, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { Gson().toJson(it) }, uuidToObject = { @@ -139,8 +47,8 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { }) foldersSync = GDriveRemoteFolder( dataType = RemoteDataType.FOLDER, - database = gDriveDatabase!!, - helper = helper, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, onPendingChange = { verifyAndNotifyPendingStateChange() }, serialiser = { Gson().toJson(it) }, uuidToObject = { @@ -148,449 +56,44 @@ class GDriveRemoteDatabase(private val weakContext: WeakReference) { }) imageSync = GDriveRemoteImageFolder( dataType = RemoteDataType.IMAGE, - database = gDriveDatabase!!, - helper = helper, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, onPendingChange = { verifyAndNotifyPendingStateChange() }) - - initRootFolder() - } - - fun isValid(): Boolean { - return isValidController - } - - fun reset() { - isValidController = false - driveHelper = null - notesSync = null - foldersSync = null - tagsSync = null - imageSync = null - } - - - fun logout() { - GlobalScope.launch { - reset() - gDriveConfig?.clearSync() - } - } - - /** - * Initialisation Methods - */ - - private fun initRootFolder() { - GlobalScope.launch { - sGDriveLastFullSyncTime = getTrueCurrentTime() - val fuid = folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER) - when { - fuid.isNotBlank() -> onRootFolderLoaded(fuid) - else -> { - driveHelper?.getOrCreateDirectory("", GOOGLE_DRIVE_ROOT_FOLDER) { - when { - (it === null) -> reset() - else -> { - folderIdForFolderName(GOOGLE_DRIVE_ROOT_FOLDER, it) - onRootFolderLoaded(it) - } - } - } - } - } - } - } - - private fun initSubRootFolder(folderName: String, folderId: String) { - when (folderName) { - FOLDER_NAME_NOTES -> notesSync?.initContentFolderId(folderId) { - if (!sGDriveFirstSyncNote) { - GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE) } - sGDriveFirstSyncNote = true - } - } - FOLDER_NAME_NOTES_META -> { - notesMetaSync?.initContentFolderId(folderId) { - if (!sGDriveFirstSyncNoteMeta) { - GlobalScope.launch { resyncDataSync(RemoteDataType.NOTE_META) } - sGDriveFirstSyncNoteMeta = true - } - } - notesMetaSync?.initDeletedFolderId(INVALID_FILE_ID) {} - } - FOLDER_NAME_TAGS -> tagsSync?.initContentFolderId(folderId) { - if (!sGDriveFirstSyncTag) { - GlobalScope.launch { resyncDataSync(RemoteDataType.TAG) } - sGDriveFirstSyncTag = true - } - } - FOLDER_NAME_FOLDERS -> foldersSync?.initContentFolderId(folderId) { - if (!sGDriveFirstSyncFolder) { - GlobalScope.launch { resyncDataSync(RemoteDataType.FOLDER) } - sGDriveFirstSyncFolder = true - } - } - FOLDER_NAME_IMAGES -> imageSync?.initContentFolderId(folderId) { - if (!sGDriveFirstSyncImage) { - GlobalScope.launch { resyncDataSync(RemoteDataType.IMAGE) } - sGDriveFirstSyncImage = true - } - } - FOLDER_NAME_DELETED_NOTES -> notesSync?.initDeletedFolderId(folderId) { - - } - FOLDER_NAME_DELETED_TAGS -> tagsSync?.initDeletedFolderId(folderId) { - - } - FOLDER_NAME_DELETED_FOLDERS -> foldersSync?.initDeletedFolderId(folderId) { - - } - } - } - - private fun onRootFolderLoaded(rootFolderId: String) { - createFolders(rootFolderId, listOf(FOLDER_NAME_NOTES, FOLDER_NAME_NOTES_META, FOLDER_NAME_FOLDERS, FOLDER_NAME_TAGS, FOLDER_NAME_IMAGES)) - createFolders(rootFolderId, listOf(FOLDER_NAME_DELETED_NOTES, FOLDER_NAME_DELETED_TAGS, FOLDER_NAME_DELETED_FOLDERS)) - } - - private fun createFolders(rootFolderId: String, expectedFolders: List) { - val knownFolderIds = expectedFolders.filter { folderIdForFolderName(it).isNotEmpty() } - knownFolderIds.forEach { - GlobalScope.launch { initSubRootFolder(it, folderIdForFolderName(it)) } - } - - val unknownFolderIds = expectedFolders.filter { !knownFolderIds.contains(it) } - if (unknownFolderIds.isEmpty()) { - return - } - - driveHelper?.getSubRootFolders(rootFolderId, unknownFolderIds)?.addOnCompleteListener { - val fileIds = it.result?.files ?: emptyList() - val existingFiles = fileIds.map { it.name } - fileIds.forEach { - GlobalScope.launch { - folderIdForFolderName(it.name, it.id) - initSubRootFolder(it.name, it.id) - } - } - unknownFolderIds.forEach { expectedFolder -> - if (!existingFiles.contains(expectedFolder)) { - driveHelper?.createFolder(rootFolderId, expectedFolder)?.addOnCompleteListener { fileIdTask -> - val file = fileIdTask.result - if (file !== null) { - folderIdForFolderName(expectedFolder, file.id) - GlobalScope.launch { initSubRootFolder(expectedFolder, file.id) } - } - } - } - } - } - } - - /** - * Pending Upload - */ - - fun setPendingUploadListener(listener: IPendingUploadListener?) { - syncListener = listener - if (listener !== null) { - verifyAndNotifyPendingStateChange() - } - } - - private fun verifyAndNotifyPendingStateChange() { - GlobalScope.launch { - val database = gDriveDatabase - if (database === null) { - return@launch - } - - val currentPendingState = database.getPendingCount() > 0 - - val pending = database.getAllPending().map { "type=${it.type}, uuid=${it.uuid}, fid=${it.fileId}" }.joinToString(separator = "\n") - log("GDrive", "getPendingCount(${database.getPendingCount()})\n$pending") - notifyPendingSyncChange("verifyAndNotifyPendingStateChange") - syncListener?.onPendingStateUpdate(currentPendingState) - } - } - - fun notifyPendingSyncChange(action: String) { - val count = sSyncingCount.get() - when { - count <= 0 -> syncListener?.onPendingSyncsUpdate(false) - count >= 1 -> syncListener?.onPendingSyncsUpdate(true) - } } - /** - * Notify local changes to the notes - */ - fun notifyChange() { - if (!isValidController) { - return + override fun getResourceIdForFolderName(folderName: String): String? { + val folderId = folderIdForFolderName(folderName) + return when { + folderId.isBlank() -> null + else -> folderId } - - verifyAndNotifyPendingStateChange() } - /** - * Resync Functions - */ - - @Synchronized - fun resync(forced: Boolean) { - if (!isValidController) { - return - } - - if (forced || (getTrueCurrentTime() - sGDriveLastFullSyncTime > 1000 * 60 * 60 * 24)) { - GlobalScope.launch { - gDriveDatabase?.resetAttempts() - initRootFolder() - } - return - } - - GlobalScope.launch { - resyncDataSync(RemoteDataType.NOTE) - resyncDataSync(RemoteDataType.NOTE_META) - resyncDataSync(RemoteDataType.TAG) - resyncDataSync(RemoteDataType.FOLDER) - resyncDataSync(RemoteDataType.IMAGE) - } + override fun storeResourceIdForFolderName(folderName: String, resource: String) { + folderIdForFolderName(folderName, resource) } - fun resyncDataSync(type: RemoteDataType) { - if (syncing[type]?.getAndSet(true) == true) { - return - } - - val pendingItems = gDriveDatabase?.getPendingByType(type.name) ?: emptyList() - for (pendingItem in pendingItems) { - if (!gDriveDbState.notifyAttempt(type, pendingItem.uuid)) { - continue - } - - val sameDelete = pendingItem.localStateDeleted == pendingItem.remoteStateDeleted - val localDeleted = pendingItem.localStateDeleted - val remoteDeleted = pendingItem.remoteStateDeleted - val sameUpdateTime = pendingItem.lastUpdateTimestamp == pendingItem.remoteUpdateTimestamp - val isLocalMoreRecent = pendingItem.lastUpdateTimestamp > pendingItem.remoteUpdateTimestamp - val isRemoteMoreRecent = pendingItem.lastUpdateTimestamp < pendingItem.remoteUpdateTimestamp - when { - sameUpdateTime -> gDriveDbState.remoteDatabaseUpdate(type, pendingItem.uuid, databaseUpdateLambda) - !sameDelete && isRemoteMoreRecent && remoteDeleted -> onRemoteRemove(type, pendingItem) - !sameDelete && isLocalMoreRecent && localDeleted -> remove(type, pendingItem) - !sameDelete && isLocalMoreRecent && remoteDeleted -> insert(type, pendingItem) - !sameDelete && isRemoteMoreRecent && localDeleted -> onRemoteInsert(type, pendingItem) - sameDelete && isLocalMoreRecent && localDeleted -> remove(type, pendingItem) - sameDelete && isLocalMoreRecent && !localDeleted -> insert(type, pendingItem) - sameDelete && isRemoteMoreRecent && localDeleted -> onRemoteRemove(type, pendingItem) - sameDelete && isRemoteMoreRecent && !localDeleted -> onRemoteInsert(type, pendingItem) - } - } - syncing[type]?.set(false) + override fun getResourceId(data: RemoteUploadData): String { + return data.uuid } - /** - * Core Data Functions - */ - - @Suppress("IMPLICIT_CAST_TO_ANY") - private fun insert(type: RemoteDataType, data: RemoteUploadData) { - if (!isValidController) { - return - } - - val logInfo = "insert(${type.name}, ${data.uuid})" - val localItem = when (type) { - RemoteDataType.NOTE -> { - ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.toExportedMarkdown()?.apply { - notesSync?.insert(data.uuid, this) - } - } - RemoteDataType.NOTE_META -> { - ApplicationBase.instance.notesDatabase().getByUUID(data.uuid)?.getExportableNoteMeta()?.apply { - notesMetaSync?.insert(data.uuid, this) - } + private fun folderIdForFolderName(folderName: String, folderId: String = ""): String { + val key = "g_drive_folder_if_for_$folderName" + if (folderId.isEmpty()) { + // Get Condition + var storedValue = remoteConfig.get(key, "") ?: "" + if (storedValue == INVALID_FILE_ID) { + remoteConfig.put(key, "") + storedValue = "" } - RemoteDataType.TAG -> { - ApplicationBase.instance.tagsDatabase().getByUUID(data.uuid)?.getExportableTag()?.apply { - tagsSync?.insert(this.uuid, this) - } - } - RemoteDataType.FOLDER -> { - ApplicationBase.instance.foldersDatabase().getByUUID(data.uuid)?.getExportableFolder()?.apply { - foldersSync?.insert(this.uuid, this) - } - } - RemoteDataType.IMAGE -> { - val imageUUID = toImageUUID(data.uuid) - if (imageUUID !== null && noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid).exists()) { - imageSync?.insert(imageUUID) - imageUUID - } else { - null - } - } - } - - if (localItem === null) { - gDriveDbState.localDatabaseRemove(type, data.uuid, {}) - } - } - - private fun remove(type: RemoteDataType, data: RemoteUploadData) { - if (!isValidController) { - return + log("GDrive", "folderIdForFolderName($folderName, $storedValue") + return storedValue } - val logInfo = "remove(${type.name}, ${data.uuid})" - log("GDriveRemote", logInfo) - val uuid = data.uuid - when (type) { - RemoteDataType.NOTE -> notesSync?.delete(uuid) - RemoteDataType.NOTE_META -> notesMetaSync?.delete(uuid) - RemoteDataType.TAG -> tagsSync?.delete(uuid) - RemoteDataType.FOLDER -> foldersSync?.delete(uuid) - RemoteDataType.IMAGE -> { - val imageUUID = toImageUUID(uuid) - when { - imageUUID !== null -> imageSync?.delete(imageUUID) - } - } - } - } - - private fun onRemoteInsert(type: RemoteDataType, data: RemoteUploadData) { - if (!isValidController) { - return - } - - val context = weakContext.get() - if (context === null) { - return - } - - when (type) { - RemoteDataType.NOTE -> { - onRemoteInsertImpl(data.fileId) { - // TODO: De-duplicate meta data and note update - try { - val itemDescription = fromExportedMarkdown(it) - val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) - ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } - val temporaryNote = NoteBuilder().copy(existingNote) - temporaryNote.description = itemDescription - IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.NOTE, data.uuid, databaseUpdateLambda) - } catch (exception: Exception) { - maybeThrow(exception) - } - } - } - RemoteDataType.NOTE_META -> { - // TODO: De-duplicate meta data and note update - onRemoteInsertImpl(data.fileId) { - try { - val item = Gson().fromJson(it, ExportableNoteMeta::class.java) - - val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) - ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } - val temporaryNote = NoteBuilder().copy(existingNote) - temporaryNote.mergeMetas(item) - IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) - - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, databaseUpdateLambda) - } catch (exception: Exception) { - maybeThrow(exception) - } - } - } - RemoteDataType.TAG -> { - onRemoteInsertImpl(data.fileId) { - try { - val item = Gson().fromJson(it, ExportableTag::class.java) - IRemoteDatabaseUtils.onRemoteInsert(context, item) - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.TAG, data.uuid, databaseUpdateLambda) - } catch (exception: Exception) { - maybeThrow(exception) - } - } - } - RemoteDataType.FOLDER -> { - onRemoteInsertImpl(data.fileId) { - try { - val item = Gson().fromJson(it, ExportableFolder::class.java) - IRemoteDatabaseUtils.onRemoteInsert(context, item) - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.FOLDER, data.uuid, databaseUpdateLambda) - } catch (exception: Exception) { - maybeThrow(exception) - } - } - } - RemoteDataType.IMAGE -> { - val imageUUID = toImageUUID(data.uuid) - if (imageUUID !== null) { - val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) - if (imageFile.exists()) { - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) - return - } - - driveHelper?.readFile(data.fileId, imageFile)?.addOnCompleteListener { - if (it.result == true) { - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) - } - } - } - } - } - } - - private fun onRemoteRemove(type: RemoteDataType, data: RemoteUploadData) { - if (!isValidController) { - return - } - - val context = weakContext.get() - if (context === null) { - return - } - - val logInfo = "onRemoteRemove(${type.name}, ${data.uuid})" - log("GDriveRemote", logInfo) - when (type) { - RemoteDataType.NOTE -> { - IRemoteDatabaseUtils.onRemoteRemoveNote(context, data.uuid) - gDriveDbState.remoteDatabaseUpdate(RemoteDataType.NOTE_META, data.uuid, databaseUpdateLambda) - } - RemoteDataType.NOTE_META -> { - } // Should never happen as note is handling this deletion - RemoteDataType.TAG -> IRemoteDatabaseUtils.onRemoteRemoveTag(context, data.uuid) - RemoteDataType.FOLDER -> IRemoteDatabaseUtils.onRemoteRemoveFolder(context, data.uuid) - RemoteDataType.IMAGE -> { - val imageUUID = toImageUUID(data.uuid) - if (imageUUID !== null) { - val imageFile = noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) - imageFile.delete() - } - } - } - gDriveDbState.remoteDatabaseUpdate(type, data.uuid, databaseUpdateLambda) - } - - /** - * Additional internal methods - */ - - private fun onRemoteInsertImpl(fileId: String, onDataAvailable: (String) -> Unit) { - driveHelper?.readFile(fileId)?.addOnCompleteListener { - val data = it.result - if (data !== null) { - onDataAvailable(data) - } + if (folderId != INVALID_FILE_ID) { + log("GDrive", "folderIdForFolderName($folderName, $folderId") + remoteConfig.put(key, folderId) } + return folderId } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index 1a7afc50..c905d88b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -1,6 +1,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.RemoteDataType +import com.bijoysingh.quicknote.database.RemoteUploadData import com.bijoysingh.quicknote.database.RemoteUploadDataDao import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -11,34 +12,67 @@ import java.util.concurrent.atomic.AtomicBoolean class GDriveRemoteFolder( dataType: RemoteDataType, database: RemoteUploadDataDao, - helper: GDriveServiceHelper, + service: GDriveServiceHelper, onPendingChange: () -> Unit, val serialiser: (T) -> String, - val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { + val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, service, onPendingChange) { - var networkOrAbsoluteFailure = AtomicBoolean(false) + private var networkOrAbsoluteFailure = AtomicBoolean(false) - var contentLoading = AtomicBoolean(true) - var contentFolderUid: String = INVALID_FILE_ID - var contentPendingActions = emptySet().toMutableSet() - val contentFiles = emptyMap().toMutableMap() + private var contentLoading = AtomicBoolean(true) + private var contentFolderUid: String = INVALID_FILE_ID + private var contentPendingActions = emptySet().toMutableSet() + private val contentFiles = emptyMap().toMutableMap() - var deletedLoading = AtomicBoolean(true) - var deletedFolderUid: String = INVALID_FILE_ID - var deletedPendingActions = emptySet().toMutableSet() - val deletedFiles = emptyMap().toMutableMap() + private var deletedLoading = AtomicBoolean(true) + private var deletedFolderUid: String = INVALID_FILE_ID + private var deletedPendingActions = emptySet().toMutableSet() + private val deletedFiles = emptyMap().toMutableMap() - val duplicateFilesToDelete: MutableList = emptyList().toMutableList() + private val duplicateFilesToDelete: MutableList = emptyList().toMutableList() - fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { + override fun initContentFolder(resourceId: String?, onSuccess: () -> Unit) { + if (resourceId === null) { + return + } + initContentFolderId(resourceId, onSuccess) + } + + override fun initDeletedFolder(resourceId: String?, onSuccess: () -> Unit) { + initDeletedFolderId(resourceId ?: INVALID_FILE_ID, onSuccess) + } + + override fun insert(remoteData: RemoteUploadData, resource: T) { + insert(remoteData.uuid, resource) + } + + override fun delete(remoteData: RemoteUploadData) { + delete(remoteData.uuid) + } + + override fun invalidate() { + networkOrAbsoluteFailure.set(false) + contentLoading.set(true) + contentFolderUid = INVALID_FILE_ID + contentPendingActions.clear() + contentFiles.clear() + + deletedLoading.set(true) + deletedFolderUid = INVALID_FILE_ID + deletedPendingActions.clear() + deletedFiles.clear() + duplicateFilesToDelete.clear() + } + + private fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { GlobalScope.launch(Dispatchers.IO) { contentLoading.set(true) contentFolderUid = fUid - helper.getFilesInFolder(contentFolderUid).addOnCompleteListener { - networkOrAbsoluteFailure.set(it.result === null) + service.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_FILE_MIME_TYPE) { filesList -> + networkOrAbsoluteFailure.set(filesList === null) - val files = it.result?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() + val files = filesList?.files ?: emptyList() files.forEach { file -> if (localFileIds.containsKey(file.name)) { duplicateFilesToDelete.add(file.id) @@ -61,7 +95,7 @@ class GDriveRemoteFolder( } } - fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { + private fun initDeletedFolderId(fUid: String, onLoaded: () -> Unit) { if (fUid == INVALID_FILE_ID) { deletedLoading.set(false) GlobalScope.launch { @@ -75,10 +109,11 @@ class GDriveRemoteFolder( GlobalScope.launch(Dispatchers.IO) { deletedLoading.set(true) deletedFolderUid = fUid - helper.getFilesInFolder(deletedFolderUid).addOnCompleteListener { - networkOrAbsoluteFailure.set(it.result === null) - val files = it.result?.files ?: emptyList() + service.getFilesInFolder(deletedFolderUid, GOOGLE_DRIVE_FILE_MIME_TYPE) { filesList -> + networkOrAbsoluteFailure.set(filesList === null) + + val files = filesList?.files ?: emptyList() val localFileIds = emptyMap().toMutableMap() files.forEach { file -> when { @@ -102,16 +137,16 @@ class GDriveRemoteFolder( } } - fun executeAllDuplicateDeletion() { + private fun executeAllDuplicateDeletion() { val files = ArrayList() files.addAll(duplicateFilesToDelete) duplicateFilesToDelete.clear() files.forEach { fileId -> - helper.removeFileOrFolder(fileId) + service.removeFileOrFolder(fileId) {} } } - fun executeInsertPendingActions() { + private fun executeInsertPendingActions() { if (deletedLoading.get() || contentLoading.get()) { return } @@ -125,7 +160,7 @@ class GDriveRemoteFolder( } } - fun executeDeletePendingActions() { + private fun executeDeletePendingActions() { if (deletedLoading.get() || contentLoading.get()) { return } @@ -137,9 +172,7 @@ class GDriveRemoteFolder( /** * Insert the file on the server based on the insertion on the local device */ - fun insert(uuid: String, item: T) { - val logInfo = "insert($uuid)" - + private fun insert(uuid: String, resource: T) { if (deletedLoading.get() || contentLoading.get()) { return } @@ -151,43 +184,39 @@ class GDriveRemoteFolder( if (deletedFiles.containsKey(uuid)) { GlobalScope.launch { val existingFileUid = deletedFiles[uuid] ?: INVALID_FILE_ID - helper.removeFileOrFolder(existingFileUid) - .addOnCompleteListener { - deletedFiles.remove(uuid) - } + service.removeFileOrFolder(existingFileUid) { success -> + if (success) { + deletedFiles.remove(uuid) + } + } } } - val data = serialiser(item) + val data = serialiser(resource) val fileId = contentFiles[uuid] val existing = database.getByUUID(dataType.name, uuid) val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() if (fileId !== null) { - helper.saveFile(fileId, uuid, data, timestamp) - .addOnCompleteListener { - val file = it.result - if (file !== null) { - notifyDriveData(file.id, uuid, timestamp) - } - } + service.updateFileWithData(fileId, uuid, data, timestamp) { file -> + if (file !== null) { + notifyDriveData(file.id, uuid, timestamp) + } + } return } - helper.createFileWithData(contentFolderUid, uuid, data, timestamp) - .addOnCompleteListener { - val file = it.result - if (file !== null) { - contentFiles[uuid] = file.id - notifyDriveData(file.id, uuid, timestamp) - } - } - + service.createFileWithData(contentFolderUid, uuid, data, timestamp) { file -> + if (file !== null) { + contentFiles[uuid] = file.id + notifyDriveData(file.id, uuid, timestamp) + } + } } /** * Delete the file on the server based on removal on the local device */ - fun delete(uuid: String) { + private fun delete(uuid: String) { if (deletedLoading.get() || contentLoading.get()) { return } @@ -208,25 +237,25 @@ class GDriveRemoteFolder( return } - helper.removeFileOrFolder(existingFileUid) - .addOnCompleteListener { - contentFiles.remove(uuid) - if (deletedFolderUid == INVALID_FILE_ID) { - return@addOnCompleteListener - } + service.removeFileOrFolder(existingFileUid) { success -> + if (!success) { + return@removeFileOrFolder + } - GlobalScope.launch { - val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp - ?: getTrueCurrentTime() - helper.createFileWithData(deletedFolderUid, uuid, uuid, timestamp) - .addOnCompleteListener { - val file = it.result - if (file !== null) { - deletedFiles[uuid] = file.id - notifyDriveData(file.id, uuid, timestamp, true) - } - } + contentFiles.remove(uuid) + if (deletedFolderUid == INVALID_FILE_ID) { + return@removeFileOrFolder + } + GlobalScope.launch { + val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp + ?: getTrueCurrentTime() + service.createFileWithData(deletedFolderUid, uuid, uuid, timestamp) { file -> + if (file !== null) { + deletedFiles[uuid] = file.id + notifyDriveData(file.id, uuid, timestamp, true) } } + } + } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 6cf65dfd..71e90058 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -1,17 +1,15 @@ package com.bijoysingh.quicknote.drive -import com.bijoysingh.quicknote.database.RemoteDataType -import com.bijoysingh.quicknote.database.RemoteDatabaseHelper -import com.bijoysingh.quicknote.database.RemoteUploadDataDao +import com.bijoysingh.quicknote.database.* import com.google.api.services.drive.model.File import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -abstract class GDriveRemoteFolderBase( +abstract class GDriveRemoteFolderBase( val dataType: RemoteDataType, val database: RemoteUploadDataDao, - val helper: GDriveServiceHelper, - val onPendingChange: () -> Unit) { + val service: GDriveServiceHelper, + val onPendingChange: () -> Unit): RemoteFolder { protected fun notifyDriveData(file: File, deleted: Boolean = false) { val modifiedTime = file.modifiedTime?.value ?: file.modifiedByMeTime?.value ?: 0L diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index bda16693..9daacea1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -1,11 +1,13 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.RemoteDataType +import com.bijoysingh.quicknote.database.RemoteUploadData import com.bijoysingh.quicknote.database.RemoteUploadDataDao import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.io.File import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -40,25 +42,62 @@ fun toImageUUID(imageUuid: String): ImageUUID? { class GDriveRemoteImageFolder( dataType: RemoteDataType, database: RemoteUploadDataDao, - helper: GDriveServiceHelper, - onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, helper, onPendingChange) { + service: GDriveServiceHelper, + onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, service, onPendingChange) { - var networkOrAbsoluteFailure = AtomicBoolean(false) + private val networkOrAbsoluteFailure = AtomicBoolean(false) - val contentLoading = AtomicBoolean(true) - var contentFolderUid: String = INVALID_FILE_ID - val contentFiles = emptyMap().toMutableMap() + private val contentLoading = AtomicBoolean(true) + private var contentFolderUid: String = INVALID_FILE_ID + private val contentFiles = emptyMap().toMutableMap() - val contentPendingActions = emptySet().toMutableSet() - val deletedPendingActions = emptySet().toMutableSet() + private val contentPendingActions = emptySet().toMutableSet() + private val deletedPendingActions = emptySet().toMutableSet() - fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { + override fun initContentFolder(resourceId: String?, onSuccess: () -> Unit) { + if (resourceId === null) { + return + } + initContentFolderId(resourceId, onSuccess) + } + + override fun initDeletedFolder(resourceId: String?, onSuccess: () -> Unit) { + // Ignore + } + + override fun insert(remoteData: RemoteUploadData, resource: File) { + val image = toImageUUID(remoteData.uuid) + if (image !== null) { + insert(image) + } + } + + override fun delete(remoteData: RemoteUploadData) { + val image = toImageUUID(remoteData.uuid) + if (image !== null) { + delete(image) + } + } + + override fun invalidate() { + networkOrAbsoluteFailure.set(false) + + contentLoading.set(true) + contentFolderUid = INVALID_FILE_ID + contentFiles.clear() + + contentPendingActions.clear() + deletedPendingActions.clear() + } + + private fun initContentFolderId(fUid: String, onLoaded: () -> Unit) { contentFolderUid = fUid GlobalScope.launch(Dispatchers.IO) { - helper.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE).addOnCompleteListener { - networkOrAbsoluteFailure.set(it.result === null) - val imageFiles = it.result?.files + service.getFilesInFolder(contentFolderUid, GOOGLE_DRIVE_IMAGE_MIME_TYPE) { filesList -> + networkOrAbsoluteFailure.set(filesList === null) + + val imageFiles = filesList?.files if (imageFiles !== null) { imageFiles.forEach { imageFile -> val components = toImageUUID(imageFile.name) @@ -74,9 +113,7 @@ class GDriveRemoteImageFolder( } } - fun insert(id: ImageUUID) { - val logInfo = "insert($id)" - + private fun insert(id: ImageUUID) { if (contentLoading.get()) { contentPendingActions.add(id) return @@ -102,18 +139,15 @@ class GDriveRemoteImageFolder( val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) - helper.createFileWithData(contentFolderUid, gDriveUUID, imageFile, timestamp) - .addOnCompleteListener { - val file = it.result - if (file !== null) { - contentFiles[id] = file.id - notifyDriveData(file.id, gDriveUUID, timestamp) - } - } + service.createFileFromFile(contentFolderUid, gDriveUUID, imageFile, timestamp) { file -> + if (file !== null) { + contentFiles[id] = file.id + notifyDriveData(file.id, gDriveUUID, timestamp) + } + } } - fun delete(id: ImageUUID) { - val logInfo = "delete($id)" + private fun delete(id: ImageUUID) { if (contentLoading.get()) { deletedPendingActions.add(id) return @@ -138,11 +172,12 @@ class GDriveRemoteImageFolder( val existing = database.getByUUID(dataType.name, id.name()) val timestamp = existing?.lastUpdateTimestamp ?: getTrueCurrentTime() - helper.removeFileOrFolder(fuid) - .addOnCompleteListener { - notifyDriveData(fuid, id.name(), timestamp, true) - contentFiles.remove(id) - } + service.removeFileOrFolder(fuid) { success -> + if (success) { + notifyDriveData(fuid, id.name(), timestamp, true) + contentFiles.remove(id) + } + } } } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index e423da7b..20216346 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -2,6 +2,7 @@ package com.bijoysingh.quicknote.drive import android.os.SystemClock import com.bijoysingh.quicknote.Scarlet.Companion.gDrive +import com.bijoysingh.quicknote.database.IRemoteService import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.api.client.http.ByteArrayContent @@ -104,20 +105,84 @@ fun getTrueCurrentTime(): Long { return calendar.timeInMillis } -class GDriveServiceHelper(private val mDriveService: Drive) { +class GDriveServiceHelper(private val mDriveService: Drive) : IRemoteService { private val mExecutor = Executors.newFixedThreadPool(4) - - fun execute(action: String = "", callable: Callable): Task { + private fun execute(action: String = "", callable: Callable): Task { return Tasks.call(mExecutor, CountingErrorCallable(action, callable)) } - fun createFileWithData(folderId: String, name: String, content: String, updateTime: Long): Task { - log("GDrive", "createFileWithData($folderId, $name)") + override fun createDirectory(parentResourceId: String?, directoryName: String, onSuccess: (String?) -> Unit) { + val parentUid = parentResourceId ?: "" + log("GDrive", "createDirectory($parentUid, $directoryName)") + execute("createDirectory", Callable { + try { + val timestamp = DateTime(getTrueCurrentTime()) + val metadata = File() + .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) + .setModifiedTime(timestamp) + .setName(directoryName) + if (!parentUid.isEmpty()) { + metadata.parents = listOf(parentUid) + } + mDriveService.files().create(metadata).execute() + } catch (exception: Exception) { + throwOrReturn(exception, null) + } + }).addOnCompleteListener { result -> + onSuccess(result.result?.id ?: INVALID_FILE_ID) + } + } + + override fun getOrCreateDirectory(parentResourceId: String?, directoryName: String, onSuccess: (String?) -> Unit) { + val parentUid = parentResourceId ?: "" + log("GDrive", "getOrCreateDirectory($parentUid, $directoryName)") + + if (parentResourceId === null) { + return createDirectory(null, directoryName, onSuccess) + } + + getDirectory(parentResourceId, directoryName).addOnCompleteListener { getTask -> + val fid = getTask.result?.files?.firstOrNull()?.id + if (fid !== null) { + onSuccess(fid) + return@addOnCompleteListener + } + + createDirectory(parentResourceId, directoryName) { uuid -> + onSuccess(uuid ?: INVALID_FILE_ID) + } + } + } + + override fun getDirectories(parentResourceId: String, directoryNames: List, onSuccess: (List>) -> Unit) { + log("GDrive", "getSubRootFolders($parentResourceId, $directoryNames)") + var nameQueryBuilder = "name = '${directoryNames[0]}'" + directoryNames.subList(1, directoryNames.lastIndex + 1).forEach { + nameQueryBuilder += " or name = '$it'" + } + execute("getSubRootFolders", Callable { + mDriveService.files().list() + .setSpaces("drive") + .setQ("mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and ($nameQueryBuilder) and '$parentResourceId' in parents") + .setOrderBy("modifiedTime desc") + .execute() + }).addOnCompleteListener { result -> + val files = result.result?.files ?: emptyList() + val namesIdList = emptyList>().toMutableList() + files.forEach { + namesIdList.add(Pair(it.name, it.id)) + } + onSuccess(namesIdList) + } + } + + override fun createFileWithData(parentResourceId: String, name: String, content: String, updateTime: Long, onSuccess: (File?) -> Unit) { + log("GDrive", "createFileWithData($parentResourceId, $name)") val contentToSave = if (content.isEmpty()) updateTime.toString() else content - return execute("createFileWithData", Callable { + execute("createFileWithData", Callable { try { val metadata = File() - .setParents(listOf(folderId)) + .setParents(listOf(parentResourceId)) .setMimeType(GOOGLE_DRIVE_FILE_MIME_TYPE) .setModifiedTime(DateTime(updateTime)) .setName(name) @@ -126,50 +191,46 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } catch (exception: Exception) { throwOrReturn(exception, null) } - }) + }).addOnCompleteListener { result -> onSuccess(result.result) } } - fun createFileWithData(folderId: String, name: String, file: java.io.File, updateTime: Long): Task { - log("GDrive", "createFileWithData($folderId, $name, ${file.absolutePath})") - return execute("createFileWithData", Callable { + override fun updateFileWithData(resourceId: String, name: String, content: String, updateTime: Long, onSuccess: (File?) -> Unit) { + log("GDrive", "saveFile($resourceId, $name)") + execute("saveFile", Callable { try { val metadata = File() - .setParents(listOf(folderId)) - .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) .setModifiedTime(DateTime(updateTime)) .setName(name) - val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, file) - mDriveService.files().create(metadata, mediaContent).execute() + val contentStream = ByteArrayContent.fromString("text/plain", content) + mDriveService.files().update(resourceId, metadata, contentStream).execute() } catch (exception: Exception) { throwOrReturn(exception, null) } - }) + }).addOnCompleteListener { result -> onSuccess(result.result) } } - fun createFolder(parentUid: String, folderName: String): Task { - log("GDrive", "createFolder($parentUid, $folderName)") - return execute("createFolder", Callable { + override fun createFileFromFile(parentResourceId: String, name: String, localFile: java.io.File, updateTime: Long, onSuccess: (File?) -> Unit) { + log("GDrive", "createFileWithData($parentResourceId, $name, ${localFile.absolutePath})") + execute("createFileWithData", Callable { try { - val timestamp = DateTime(getTrueCurrentTime()) val metadata = File() - .setMimeType(GOOGLE_DRIVE_FOLDER_MIME_TYPE) - .setModifiedTime(timestamp) - .setName(folderName) - if (!parentUid.isEmpty()) { - metadata.parents = listOf(parentUid) - } - mDriveService.files().create(metadata).execute() + .setParents(listOf(parentResourceId)) + .setMimeType(GOOGLE_DRIVE_IMAGE_MIME_TYPE) + .setModifiedTime(DateTime(updateTime)) + .setName(name) + val mediaContent = FileContent(GOOGLE_DRIVE_IMAGE_MIME_TYPE, localFile) + mDriveService.files().create(metadata, mediaContent).execute() } catch (exception: Exception) { throwOrReturn(exception, null) } - }) + }).addOnCompleteListener { result -> onSuccess(result.result) } } - fun readFile(fileId: String): Task { - log("GDrive", "readFile($fileId)") - return execute("readFile", Callable { + override fun readFile(resourceId: String, onRead: (String) -> Unit) { + log("GDrive", "readFile($resourceId)") + execute("readFile", Callable { try { - mDriveService.files().get(fileId).executeMediaAsInputStream().use { `is` -> + mDriveService.files().get(resourceId).executeMediaAsInputStream().use { `is` -> BufferedReader(InputStreamReader(`is`)).use { reader -> reader.readText() } @@ -177,53 +238,57 @@ class GDriveServiceHelper(private val mDriveService: Drive) { } catch (exception: Exception) { throwOrReturn(exception, null) } - }) + }).addOnCompleteListener { result -> + onRead(result.result ?: "") + } } - fun readFile(fileId: String, destinationFile: java.io.File): Task { - log("GDrive", "readFile($fileId, ${destinationFile.absolutePath})") - return execute("readFile", Callable { + override fun readIntoFile(resourceId: String, destinationFile: java.io.File, onRead: (Boolean) -> Unit) { + log("GDrive", "readFile($resourceId, ${destinationFile.absolutePath})") + execute("readFile", Callable { destinationFile.parentFile.mkdirs() try { val fileStream = FileOutputStream(destinationFile) - mDriveService.files().get(fileId).executeMediaAndDownloadTo(fileStream) + mDriveService.files().get(resourceId).executeMediaAndDownloadTo(fileStream) fileStream.close() } catch (exception: Exception) { return@Callable throwOrReturn(exception, false) } destinationFile.exists() - }) + }).addOnCompleteListener { result -> + onRead(result.result ?: false) + } } - fun saveFile(fileId: String, name: String, content: String, updateTime: Long): Task { - log("GDrive", "saveFile($fileId, $name)") - return execute("saveFile", Callable { + override fun removeFileOrFolder(resourceId: String, onSuccess: (Boolean) -> Unit) { + log("GDrive", "removeFileOrFolder($resourceId)") + execute("removeFileOrFolder", Callable { try { - val metadata = File() - .setModifiedTime(DateTime(updateTime)) - .setName(name) - val contentStream = ByteArrayContent.fromString("text/plain", content) - mDriveService.files().update(fileId, metadata, contentStream).execute() + mDriveService.files().delete(resourceId).execute() + true } catch (exception: Exception) { - throwOrReturn(exception, null) + maybeThrow(exception) + false } - }) + }).addOnCompleteListener { result -> onSuccess(result.result ?: false) } } - fun getFilesInFolder(parentUid: String, mimeType: String = GOOGLE_DRIVE_FILE_MIME_TYPE): Task { - log("GDrive", "getFilesInFolder($parentUid, $mimeType)") - return execute("getFilesInFolder", Callable { + override fun getFilesInFolder(parentResourceId: String, mimeType: String, onSuccess: (FileList?) -> Unit) { + log("GDrive", "getFilesInFolder($parentResourceId, $mimeType)") + execute("getFilesInFolder", Callable { mDriveService.files().list() .setSpaces("drive") .setPageSize(1000) .setFields("files(name, id, modifiedTime, mimeType)") - .setQ("mimeType = '$mimeType' and '$parentUid' in parents") + .setQ("mimeType = '$mimeType' and '$parentResourceId' in parents") .setOrderBy("modifiedTime desc") .execute() - }) + }).addOnCompleteListener { result -> + onSuccess(result.result) + } } - fun getFolderQuery(parentUid: String, name: String): Task { + private fun getDirectory(parentUid: String, name: String): Task { log("GDrive", "getFolderQuery($parentUid, $name)") val query = when { parentUid.isEmpty() -> "mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and name = '$name'" @@ -238,46 +303,4 @@ class GDriveServiceHelper(private val mDriveService: Drive) { }) } - fun getSubRootFolders(parentUid: String, names: List): Task { - log("GDrive", "getSubRootFolders($parentUid, $names)") - var nameQueryBuilder = "name = '${names[0]}'" - names.subList(1, names.lastIndex + 1).forEach { - nameQueryBuilder += " or name = '$it'" - } - return execute("getSubRootFolders", Callable { - mDriveService.files().list() - .setSpaces("drive") - .setQ("mimeType = '$GOOGLE_DRIVE_FOLDER_MIME_TYPE' and ($nameQueryBuilder) and '$parentUid' in parents") - .setOrderBy("modifiedTime desc") - .execute() - }) - } - - fun removeFileOrFolder(fileUid: String): Task { - log("GDrive", "removeFileOrFolder($fileUid)") - return execute("removeFileOrFolder", Callable { - try { - mDriveService.files().delete(fileUid).execute() - true - } catch (exception: Exception) { - maybeThrow(exception) - false - } - }) - } - - fun getOrCreateDirectory(parentUid: String, name: String, onFolderId: (String?) -> Unit) { - log("GDrive", "getOrCreateDirectory($parentUid, $name)") - getFolderQuery(parentUid, name).addOnCompleteListener { getTask -> - val fid = getTask.result?.files?.firstOrNull()?.id - if (fid !== null) { - onFolderId(fid) - return@addOnCompleteListener - } - - createFolder(parentUid, name).addOnCompleteListener { createTask -> - onFolderId(createTask.result?.id ?: INVALID_FILE_ID) - } - } - } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 454f1ef8..82e4aea2 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -16,13 +16,13 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" var sGDriveLoggedIn: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit + get() = Scarlet.remoteConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false + set(value) = Scarlet.remoteConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit const val KEY_FIREBASE_KILLED = "firebase_killed_v2" var sFirebaseKilled: Boolean - get() = Scarlet.gDriveConfig?.get(KEY_FIREBASE_KILLED, false) ?: false - set(value) = Scarlet.gDriveConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit + get() = Scarlet.remoteConfig?.get(KEY_FIREBASE_KILLED, false) ?: false + set(value) = Scarlet.remoteConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit class ScarletAuthenticator() : IAuthenticator { val firebase = FirebaseAuthenticator() From 5c18a21b2af1f0e06fad192c97cdbe22461eb5f0 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 16 Oct 2019 21:45:55 +0100 Subject: [PATCH 090/134] Fixing GDrive bugs due to WebDAV abstraction changes --- .../quicknote/database/RemoteController.kt | 4 ++-- .../quicknote/drive/GDriveRemoteDatabase.kt | 2 +- .../quicknote/drive/GDriveRemoteImageFolder.kt | 9 +++++++++ .../quicknote/drive/GDriveServiceHelper.kt | 14 +++++--------- .../quicknote/scarlet/ScarletAuthenticator.kt | 8 ++++---- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt index 9c4ac24e..25a95767 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt @@ -170,7 +170,7 @@ abstract class RemoteController(private val } remoteService.getDirectories(rootFolderId, unknownFolderNames) { items -> - val pendingNames = emptyList().toMutableList() + val pendingNames = unknownFolderNames.toSet().toMutableSet() items.forEach { pair -> val name = pair.first val resourceId = pair.second @@ -178,8 +178,8 @@ abstract class RemoteController(private val resourceId !== null -> { storeResourceIdForFolderName(name, resourceId) initSubRootFolder(name, resourceId) + pendingNames.remove(name) } - else -> pendingNames.add(name) } } pendingNames.forEach { name -> diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 76b4b6d1..979ab87a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -74,7 +74,7 @@ class GDriveRemoteDatabase(weakContext: WeakReference) : RemoteControll } override fun getResourceId(data: RemoteUploadData): String { - return data.uuid + return data.fileId } private fun folderIdForFolderName(folderName: String, folderId: String = ""): String { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 9daacea1..93e8cdc1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -139,6 +139,15 @@ class GDriveRemoteImageFolder( val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) + if (!imageFile.exists()) { + // notifyDriveData(id, gDriveUUID, timestamp) + val existing = database.getByUUID(RemoteDataType.IMAGE.name, gDriveUUID) + if (existing !== null) { + database.delete(existing) + } + onPendingChange() + return + } service.createFileFromFile(contentFolderUid, gDriveUUID, imageFile, timestamp) { file -> if (file !== null) { contentFiles[id] = file.id diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index 20216346..a9d22d5f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -134,14 +134,7 @@ class GDriveServiceHelper(private val mDriveService: Drive) : IRemoteService Unit) { - val parentUid = parentResourceId ?: "" - log("GDrive", "getOrCreateDirectory($parentUid, $directoryName)") - - if (parentResourceId === null) { - return createDirectory(null, directoryName, onSuccess) - } - - getDirectory(parentResourceId, directoryName).addOnCompleteListener { getTask -> + getDirectory(parentResourceId ?: "", directoryName).addOnCompleteListener { getTask -> val fid = getTask.result?.files?.firstOrNull()?.id if (fid !== null) { onSuccess(fid) @@ -170,7 +163,10 @@ class GDriveServiceHelper(private val mDriveService: Drive) : IRemoteService>().toMutableList() files.forEach { - namesIdList.add(Pair(it.name, it.id)) + when { + (it.id === null || it.id.isBlank()) -> {} + else -> namesIdList.add(Pair(it.name, it.id)) + } } onSuccess(namesIdList) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 82e4aea2..91e81a8b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -16,13 +16,13 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity const val KEY_G_DRIVE_LOGGED_IN = "g_drive_logged_in" var sGDriveLoggedIn: Boolean - get() = Scarlet.remoteConfig?.get(KEY_G_DRIVE_LOGGED_IN, false) ?: false - set(value) = Scarlet.remoteConfig?.put(KEY_G_DRIVE_LOGGED_IN, value) ?: Unit + get() = Scarlet.remoteConfig.get(KEY_G_DRIVE_LOGGED_IN, false) + set(value) = Scarlet.remoteConfig.put(KEY_G_DRIVE_LOGGED_IN, value) const val KEY_FIREBASE_KILLED = "firebase_killed_v2" var sFirebaseKilled: Boolean - get() = Scarlet.remoteConfig?.get(KEY_FIREBASE_KILLED, false) ?: false - set(value) = Scarlet.remoteConfig?.put(KEY_FIREBASE_KILLED, value) ?: Unit + get() = Scarlet.remoteConfig.get(KEY_FIREBASE_KILLED, false) + set(value) = Scarlet.remoteConfig.put(KEY_FIREBASE_KILLED, value) class ScarletAuthenticator() : IAuthenticator { val firebase = FirebaseAuthenticator() From b0991b2c46beaf3eba5900969aa6d455e5187560 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 16 Oct 2019 22:24:17 +0100 Subject: [PATCH 091/134] [Enhancements] Adding black and 5 other note colors --- base/src/main/res/values/colors.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/base/src/main/res/values/colors.xml b/base/src/main/res/values/colors.xml index 64ca7ccc..b754efa5 100644 --- a/base/src/main/res/values/colors.xml +++ b/base/src/main/res/values/colors.xml @@ -46,14 +46,18 @@ #99907d + @color/black @color/material_grey_850 @color/material_blue_grey_800 + @color/material_blue_grey_700 @color/material_purple_700 @color/material_deep_purple_700 @color/material_indigo_700 + @color/material_blue_900 @color/material_blue_700 @color/material_light_blue_700 @color/material_cyan_700 + @color/material_cyan_800 @color/material_teal_700 @color/material_green_700 @color/material_light_green_700 @@ -64,6 +68,8 @@ @color/material_deep_orange_700 @color/material_pink_700 @color/material_red_700 + @color/material_red_900 + @color/material_brown_700 From a5db692f81dae847171ec7a26cd2c332597d62e3 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 16 Oct 2019 22:55:24 +0100 Subject: [PATCH 092/134] Adding back the translatable false --- base/src/main/res/values/strings.xml | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index cb7d68e6..6394a244 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -115,10 +115,10 @@ Security Options Pass Code Set a 4 digit PIN to lock notes - Enable App Lock - Use PIN to unlock the whole application - Always ask for PIN - Enter PIN for all notes, even if previously entered + Enable App Lock + Use PIN to unlock the whole application + Always ask for PIN + Enter PIN for all notes, even if previously entered Unlock with Fingerprint The notes will unlock with the fingerprint Fingerprint disabled @@ -204,8 +204,8 @@ Sort Notes By Newest First - Note Color - Note Tags + Note Color + Note Tags Oldest First Most Recently Modified Alphabetical @@ -229,12 +229,12 @@ More Widget Settings. Easier Note Selection. - Faster Updates and Requests. - App Lock and Single Unlock. - More App Themes. - Note Font Size Selection. - Viewer Background Color. - More Widget Options. + Faster Updates and Requests. + App Lock and Single Unlock. + More App Themes. + Note Font Size Selection. + Viewer Background Color. + More Widget Options. Copy Block Text Actions @@ -274,7 +274,7 @@ Are you sure? Would you like to permanently delete the notes in the trash folder? Would you like to permanently delete this note? - Would you like to permanently delete these notes? + Would you like to permanently delete these notes? Delete Cancel Notes are deleted forever after 7 days @@ -305,8 +305,8 @@ - Forget Me - Logout and remove all online data + Forget Me + Logout and remove all online data Scarlet @@ -378,11 +378,11 @@ Show Notes in Trash Allow notes in trash to be shown in home screen widgets - Widget Background - Choose the background color for the multi-notes widget + Widget Background + Choose the background color for the multi-notes widget - Show Action Toolbar - Multi-notes widget shows the toolbar + Show Action Toolbar + Multi-notes widget shows the toolbar @@ -392,13 +392,13 @@ Syncing Pending Backup - Local - Remote + Local + Remote - Unavailable - Created - Deleted - Updated + Unavailable + Created + Deleted + Updated Developer Options @@ -468,9 +468,9 @@ - Unlock App - Enter 4 digit PIN or use fingerprint to unlock - Enter 4 digit PIN to unlock + Unlock App + Enter 4 digit PIN or use fingerprint to unlock + Enter 4 digit PIN to unlock From 33948ec05c93474b8c14586292eee7882264e101 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 18 Oct 2019 23:23:11 +0100 Subject: [PATCH 093/134] Adding Note Shortcut Support #149 --- base/src/main/AndroidManifest.xml | 42 +++++++++------ .../note/actions/NoteOptionsBottomSheet.kt | 49 +++++++++++++++++- .../activity/NoteIntentRouterActivity.kt | 33 ++++++++++++ base/src/main/res/drawable/icon_shortcut.png | Bin 0 -> 772 bytes .../res/mipmap-hdpi/open_note_launcher.png | Bin 0 -> 3839 bytes .../res/mipmap-mdpi/open_note_launcher.png | Bin 0 -> 2031 bytes .../res/mipmap-xhdpi/open_note_launcher.png | Bin 0 -> 4512 bytes .../res/mipmap-xxhdpi/open_note_launcher.png | Bin 0 -> 8287 bytes .../res/mipmap-xxxhdpi/open_note_launcher.png | Bin 0 -> 9893 bytes base/src/main/res/values/strings.xml | 1 + 10 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt create mode 100644 base/src/main/res/drawable/icon_shortcut.png create mode 100644 base/src/main/res/mipmap-hdpi/open_note_launcher.png create mode 100644 base/src/main/res/mipmap-mdpi/open_note_launcher.png create mode 100644 base/src/main/res/mipmap-xhdpi/open_note_launcher.png create mode 100644 base/src/main/res/mipmap-xxhdpi/open_note_launcher.png create mode 100644 base/src/main/res/mipmap-xxxhdpi/open_note_launcher.png diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index 63e9b81c..5236dbbe 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -14,8 +14,9 @@ + @@ -24,26 +25,36 @@ android:resource="@xml/provider_paths" /> - - + + - - + + + + + + + + + + + + @@ -61,7 +72,7 @@ - + @@ -70,8 +81,7 @@ android:name="android.appwidget.provider" android:resource="@xml/widget_info" /> - - + @@ -81,20 +91,20 @@ android:resource="@xml/create_note_widget_info" /> - + - - + + diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 3edfd8e5..bfc4ec55 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -1,7 +1,13 @@ package com.maubis.scarlet.base.note.actions import android.app.Dialog +import android.app.PendingIntent import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build import android.support.v4.content.ContextCompat import android.view.View import android.widget.GridLayout @@ -33,12 +39,16 @@ import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.FlavorUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch +import java.util.* +import kotlin.collections.ArrayList + class NoteOptionsBottomSheet() : GridBottomSheetBase() { @@ -342,6 +352,43 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { visible = note.getNoteState() !== NoteState.TRASH, invalid = activity.lockedContentIsHidden() && note.locked )) + + options.add(OptionsItem( + title = R.string.pin_to_launcher, + subtitle = R.string.pin_to_launcher, + icon = R.drawable.icon_shortcut, + listener = View.OnClickListener { + if (!FlavorUtils.isLite() && Build.VERSION.SDK_INT >= 26) { + var title = note.getTitleForSharing() + if (title.isBlank()) { + title = note.getFullText().split("\n").firstOrNull() ?: "Note" + } + + val shortcutManager = activity.getSystemService(ShortcutManager::class.java) + val shortcut = ShortcutInfo.Builder(activity, note.uuid) + .setShortLabel(title) + .setLongLabel(title) + .setIcon(Icon.createWithResource(activity, R.mipmap.open_note_launcher)) + .setIntent(Intent(Intent.ACTION_VIEW, + Uri.parse("scarlet://open_note?uuid=" + note.uuid))) + .build() + + shortcutManager.dynamicShortcuts = Arrays.asList(shortcut) + if (shortcutManager.isRequestPinShortcutSupported) { + val pinShortcutInfo = ShortcutInfo.Builder(activity, note.uuid).build() + val pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo) + + val successCallback = PendingIntent.getBroadcast(activity, 0, + pinnedShortcutCallbackIntent, 0) + shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) + } + return@OnClickListener + } + openSheet(activity, InstallProUpsellBottomSheet()) + }, + visible = Build.VERSION.SDK_INT >= 26, + invalid = activity.lockedContentIsHidden() && note.locked + )) options.add(OptionsItem( title = R.string.reminder, subtitle = R.string.reminder, @@ -385,7 +432,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { note.viewDistractionFree(activity) return@OnClickListener } - com.maubis.scarlet.base.support.sheets.openSheet(activity, InstallProUpsellBottomSheet()) + openSheet(activity, InstallProUpsellBottomSheet()) }, invalid = activity.lockedContentIsHidden() && note.locked )) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt new file mode 100644 index 00000000..3e5f0740 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt @@ -0,0 +1,33 @@ +package com.maubis.scarlet.base.note.creation.activity + +import android.net.Uri +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance + +class NoteIntentRouterActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val data: Uri? = intent?.data + if (data === null) { + finish() + return + } + + val noteUUID = data.getQueryParameter("uuid") + if (noteUUID === null) { + finish() + return + } + + val note = instance.notesDatabase().getByUUID(noteUUID) + if (note === null) { + finish() + return + } + + startActivity(ViewAdvancedNoteActivity.getIntent(this, note)) + finish() + } +} diff --git a/base/src/main/res/drawable/icon_shortcut.png b/base/src/main/res/drawable/icon_shortcut.png new file mode 100644 index 0000000000000000000000000000000000000000..5753ef1657346f401ece71880cb3405e3b738606 GIT binary patch literal 772 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 N002ovPDHLkV1hlg;*J0S literal 0 HcmV?d00001 diff --git a/base/src/main/res/mipmap-xhdpi/open_note_launcher.png b/base/src/main/res/mipmap-xhdpi/open_note_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..eab29dbd5aed6d71db261efca4bd0765a56812fb GIT binary patch literal 4512 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 KPNlYeChange Tags Unlock Note Reminder + Launcher Shortcut Select Duplicate Pin Note From 9b57ea508a734ad68e8720c0f68afc47301a046e Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Fri, 18 Oct 2019 23:57:57 +0100 Subject: [PATCH 094/134] LongPress Add Note creates shortcut on homescreen #149 --- base/src/main/AndroidManifest.xml | 1 + .../main/specs/MainActivityBottomBarSpec.kt | 20 +++++++++++++ .../note/actions/NoteOptionsBottomSheet.kt | 15 ++-------- .../activity/NoteIntentRouterActivity.kt | 26 +++++++++++++---- .../scarlet/base/support/ShortcutUtils.kt | 27 ++++++++++++++++++ .../base/support/specs/RoundIconSpec.kt | 15 +++++++--- .../main/res/mipmap-hdpi/create_launcher.png | Bin 0 -> 3538 bytes .../res/mipmap-hdpi/open_note_launcher.png | Bin 3839 -> 3898 bytes .../main/res/mipmap-mdpi/create_launcher.png | Bin 0 -> 1882 bytes .../res/mipmap-mdpi/open_note_launcher.png | Bin 2031 -> 2062 bytes .../main/res/mipmap-xhdpi/create_launcher.png | Bin 0 -> 4333 bytes .../res/mipmap-xhdpi/open_note_launcher.png | Bin 4512 -> 4581 bytes .../res/mipmap-xxhdpi/create_launcher.png | Bin 0 -> 7874 bytes .../res/mipmap-xxhdpi/open_note_launcher.png | Bin 8287 -> 8333 bytes .../res/mipmap-xxxhdpi/create_launcher.png | Bin 0 -> 9617 bytes .../res/mipmap-xxxhdpi/open_note_launcher.png | Bin 9893 -> 10087 bytes 16 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt create mode 100644 base/src/main/res/mipmap-hdpi/create_launcher.png create mode 100644 base/src/main/res/mipmap-mdpi/create_launcher.png create mode 100644 base/src/main/res/mipmap-xhdpi/create_launcher.png create mode 100644 base/src/main/res/mipmap-xxhdpi/create_launcher.png create mode 100644 base/src/main/res/mipmap-xxxhdpi/create_launcher.png diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index 5236dbbe..21bc57dd 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -46,6 +46,7 @@ + diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index cd50cc6b..93e35070 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -1,6 +1,11 @@ package com.maubis.scarlet.base.main.specs +import android.content.Intent +import android.content.pm.ShortcutInfo import android.graphics.Color +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build import android.text.Layout import com.facebook.litho.* import com.facebook.litho.annotations.LayoutSpec @@ -23,6 +28,7 @@ import com.maubis.scarlet.base.main.sheets.HomeOptionsBottomSheet import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.note.folder.sheet.CreateOrEditFolderBottomSheet +import com.maubis.scarlet.base.support.addShortcut import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.specs.ToolbarColorConfig @@ -70,6 +76,20 @@ object MainActivityBottomBarSpec { }) row.child(bottomBarRoundIcon(context, colorConfig) .iconRes(R.drawable.icon_add_note) + .isLongClickEnabled(true) + .onLongClick { + if (Build.VERSION.SDK_INT < 26) { + return@onLongClick + } + + val shortcut = ShortcutInfo.Builder(activity, "scarlet_notes___create_note") + .setShortLabel(activity.getString(R.string.shortcut_add_note)) + .setLongLabel(activity.getString(R.string.shortcut_add_note)) + .setIcon(Icon.createWithResource(activity, R.mipmap.create_launcher)) + .setIntent(Intent(Intent.ACTION_VIEW, Uri.parse("scarlet://create_note"))) + .build() + addShortcut(activity, shortcut) + } .onClick { val intent = CreateNoteActivity.getNewNoteIntent( activity, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index bfc4ec55..051be1ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -37,6 +37,7 @@ import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController +import com.maubis.scarlet.base.support.addShortcut import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase import com.maubis.scarlet.base.support.sheets.openSheet @@ -364,24 +365,14 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { title = note.getFullText().split("\n").firstOrNull() ?: "Note" } - val shortcutManager = activity.getSystemService(ShortcutManager::class.java) - val shortcut = ShortcutInfo.Builder(activity, note.uuid) + val shortcut = ShortcutInfo.Builder(activity, "scarlet_notes___${note.uuid}") .setShortLabel(title) .setLongLabel(title) .setIcon(Icon.createWithResource(activity, R.mipmap.open_note_launcher)) .setIntent(Intent(Intent.ACTION_VIEW, Uri.parse("scarlet://open_note?uuid=" + note.uuid))) .build() - - shortcutManager.dynamicShortcuts = Arrays.asList(shortcut) - if (shortcutManager.isRequestPinShortcutSupported) { - val pinShortcutInfo = ShortcutInfo.Builder(activity, note.uuid).build() - val pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo) - - val successCallback = PendingIntent.getBroadcast(activity, 0, - pinnedShortcutCallbackIntent, 0) - shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) - } + addShortcut(activity, shortcut) return@OnClickListener } openSheet(activity, InstallProUpsellBottomSheet()) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt index 3e5f0740..c4ac4049 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt @@ -15,19 +15,35 @@ class NoteIntentRouterActivity : AppCompatActivity() { return } + handleOpenNote(data) + handleCreateNote(data) + } + + fun handleOpenNote(data: Uri): Boolean { + if (data.host != "open_note") { + return false + } + val noteUUID = data.getQueryParameter("uuid") if (noteUUID === null) { - finish() - return + return false } val note = instance.notesDatabase().getByUUID(noteUUID) if (note === null) { - finish() - return + return false } startActivity(ViewAdvancedNoteActivity.getIntent(this, note)) - finish() + return true + } + + fun handleCreateNote(data: Uri): Boolean { + if (data.host != "create_note") { + return false + } + + startActivity(CreateNoteActivity.getNewNoteIntent(this)) + return true } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt new file mode 100644 index 00000000..af48bae8 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt @@ -0,0 +1,27 @@ +package com.maubis.scarlet.base.support + +import android.app.PendingIntent +import android.content.Context +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.os.Build +import java.util.* + +fun addShortcut(context: Context, shortcut: ShortcutInfo) { + if (Build.VERSION.SDK_INT < 26) { + return + } + + val shortcutManager = context.getSystemService(ShortcutManager::class.java) + shortcutManager.dynamicShortcuts = Arrays.asList(shortcut) + + if (shortcutManager.isRequestPinShortcutSupported) { + val pinShortcutInfo = ShortcutInfo.Builder(context, shortcut.id).build() + val pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo) + + val successCallback = PendingIntent.getBroadcast(context, 0, + pinnedShortcutCallbackIntent, 0) + shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt index fba1fe47..4df970d2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt @@ -2,10 +2,7 @@ package com.maubis.scarlet.base.support.specs import android.graphics.Color import android.graphics.drawable.Drawable -import com.facebook.litho.ClickEvent -import com.facebook.litho.Column -import com.facebook.litho.Component -import com.facebook.litho.ComponentContext +import com.facebook.litho.* import com.facebook.litho.annotations.* import com.facebook.litho.widget.Image import com.facebook.yoga.YogaAlign @@ -27,6 +24,7 @@ object RoundIconSpec { @Prop(optional = true) iconAlpha: Float?, @Prop(optional = true) bgAlpha: Int?, @Prop(optional = true) isClickDisabled: Boolean?, + @Prop(optional = true) isLongClickEnabled: Boolean?, @Prop(optional = true) showBorder: Boolean?): Component { val image = Image.create(context) .heightPx(iconSize) @@ -40,6 +38,9 @@ object RoundIconSpec { if (isClickDisabled === null || !isClickDisabled) { image.clickHandler(RoundIcon.onClickEvent(context)) } + if (isLongClickEnabled !== null && isLongClickEnabled) { + image.longClickHandler(RoundIcon.onLongClickEvent(context)) + } return Column.create(context) .alignItems(YogaAlign.CENTER) .child(image) @@ -50,4 +51,10 @@ object RoundIconSpec { fun onClickEvent(context: ComponentContext, @Prop(optional = true) onClick: () -> Unit) { onClick() } + + @OnEvent(LongClickEvent::class) + fun onLongClickEvent(context: ComponentContext, @Prop(optional = true) onLongClick: () -> Unit): Boolean { + onLongClick() + return true + } } diff --git a/base/src/main/res/mipmap-hdpi/create_launcher.png b/base/src/main/res/mipmap-hdpi/create_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9d329a83b70acd79ce8fbb856d9eb9c4fb7a7e7c GIT binary patch literal 3538 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 M07*qoM6N<$g4|NOoB#j- literal 0 HcmV?d00001 diff --git a/base/src/main/res/mipmap-hdpi/open_note_launcher.png b/base/src/main/res/mipmap-hdpi/open_note_launcher.png index 3e27b03cca81abd997e13b2225a059076edb60c7..b45793eaf6a6dd0afd48201de0d455a990b38102 100644 GIT binary patch literal 3898 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 UUUkR)*8l(j07*qoM6N<$f>6JPS^xk5 literal 0 HcmV?d00001 diff --git a/base/src/main/res/mipmap-mdpi/open_note_launcher.png b/base/src/main/res/mipmap-mdpi/open_note_launcher.png index 39ee7f5d845869de9686b8ed036d64d37e7015bd..f07e82fbb51c15e9b2f96c6f0e27476d46eda0eb 100644 GIT binary patch delta 2048 zcmV+b2>@gKW zqASQAQ%QsKYkwyP(i;1$xlcDjc!%ahgaOdlAKTpK%xL|SBe|9wiR;OcP(zLc1$ch~ z(G_Gjn#pc7lg(%*M@s!)EIF+mU$=b@l; zrS5G2x|#B8ZQ1R+{KC&>{VTz1OUbG$rT)~q8NCzgquDK6rj^ZH?!LCvD{&}w{j(4b z!jP8g?SEypE1jv0*#k$Ra*j;5;FltyML+S&pyzB;6;J4kxwIn_pCEj+XD*p z8_WL+zz9F5wYq0H6c~uGVsuJ3lSk~t6UC{TZ)*V%X z_6P21+VQg&yb#w8kI=~SO+s=u(MZnILb8PnXKfU+tb;x({C?O1NA;!sO1;1u7pzJd zNPn)q8xVY64Sl@+$25NKB8`1`&O33W0#^^%LY_QB=}j_$E?)r|JkL1dH6{P4NnWu~ zUPkJ7OcfjaK}&bt-O)`%MEC!7OHts?->%WMBd2_GctkP(?Egqvz|ZdUj3b_Lv?r!` zS{dp37F7Nw5crfp*OY)b>!kvb^+4CO-G5gScwf)G0of%+)bXSmhZ3;-fO5A77Px)! zDqY=g^UXf1V!r5gP8SHW`#j_5h_+-1K$N%s0mNDosxJis_a|VFsqpQA{kFg-uH@8p zrpbDsy({SM^NgcoTJAD{SWh1?0Q0N~H8+%i<(CHT_D-*Wr&*-}U-UX@D6PpGXMb9w z5R;gN4MI{I=v3}A0R@!cdB)KR^}2BY^P~ca_W0_1{-C9Cw|N9^dMC}S^+0>qba({; zi>l-SfF!9vk}a#O>eIygtcne5)0fee zcP&8$0+o(;3aN`P14xt##P`LN4*P?aCYZp)=oPy1F0T*^O`N|Rx_}>eo^jO63Qqxu zm!8Ja`;8@glz`=z1Fn`S0ndp1xBGlRuuzw)c}Gxqo^kY^y5JoE(NY2Z&wma1-%$dV z$18ASO5oBfzl|9F!JSDON^1&9;1}w==KvU_C$T2m5TD5#%P0MTdthBHJEjE0Iv24y*I-bzG75C8j+?)~G2EMwkG2FaI0pM(wH%L+#Uq{++2=c2A# zoA-iM4ldbcyU|S7Pr4#*+VQg&!h+`+N8MPt4M3dI?t7LNfT>5f_J6dzJfvk}i!#%h zs_o?N=oZuEn|4{+I=%5cW;s*2EiCvRR`5pv=?LDSu)y}Zs3upBxV_yYvkfQ8~~&e|uI z48r_P0I+PkD(l4q?8!?O6&De-XUZI4Ph9#TvR(p^CBBAsHsPg94WXeH0Lb|PGPdqP z{>ekE=)NV)20g@z?!Srr;pee*8-N@DDdOJ_c~s7HG4Vx=5N3165=0e#4;fo`W7Wa8 zkpKH0R`7W*D|x)t$BG{Gv7!gPtngM3EBO2^JpPAW$UE>nGG0hW)RPdJbiVkv=IqmY zsfY@~e2x`m(g9>b*wI`7tHk7rU(19r)04&D(L>mbNB$2W9vKY;p(qHu8ZE{seoZI- e%&h&B$NvD?ADE@}Es{V00000Y5O$? z5or>oU<_$Qo1{`ARe_{ZAqPn-(Fll3If{v~jd%9&Ue4O!oxRPjqaKjf7PO73f}NDa z2_yK_>VDZHMvA`4M z7ki?+zSS9ebj2$>i*Xl)G+4P(x6%{*{$g*etJ)X8T$t}f_|_`cx`J`Ujs&5DQyfcBf7a6^qR{+DuhluBJunkz;QpH85_ z8K!(kDj}Nhj=o>#3^ab-BFuel_Sz-Bc)Y?RfXm^hBVcxf$m9r7zB5FX?r?nhLC3ui zN^3@kc7>CFs@fC3SmusWnL9TDDSS%6b%kdi_#&WXC*D z{ARg3M%f5tGW&7?qh0vf;^dnE=yHd5MVc$zG0GM`7lAx`kn-$7n(quA0-&ERy>4k| zXlFY3nXG>$c!T&j&)z$&Bf-{k-5cd)Gn=z-kVsUubv+GXBg|+p^*atNtMbIhrh=ah z0lh6qCVzWy+_L?^3S_uC85X)ieM;bJ@Jsv&T9+K4#{R+ENeSL_8yI2yH2@`W9KfA*%3-B zptA+3yxFx;t&}uCrODz7|5XaD2A>k>KR=oie1B@Jik(muFk5^74#0#oyER+$TgMtT za3%QS&Ip|v9h(t>4gG^@;k6u3T8^i(z1D_wN5N3V^>!;krvpzltyI8&_B<`?PSUdO zB;6~?QooQoe}d}#399uc=)3&yvlcM6b@wXuf~Nd|$1BQRkt=H8C9ViP5`T|QeK|(I z`+t1&)`}w)unmu7yz*50`X&jimkG$>S&k>YjsH*S!w=7wml0dNyY-25(9&67-#6#782MaLAl>YS}jweH_ zzYl;>vV5YT)D{?415YO~#~G%RvK~;Yd+Dxg@x~Pe)MlUMcrvv2d<38%)dv~?MRV=J zFO`7hrv_4iRI^G2JRg2a)!x2av3TQT@y4l2Or<2tkMvIxXp{*k!LuAsx{jk`0Dp?4 z0wu-H@I*RjX&RH01qx&Wxs1mC!8_oU1hib>27tLzfw@JF(1a4O{G@DMa^RLg!7K>K z;aQF+wj*%;P61nBG##`wQwjLbo~QbrB-QsMX_=VRX4du3a~TP!!gD-n4{Mid*?Xnzk;$C;0(6i_RjTRqU~{|G>dRG_e+#g|A2U66&q znXz%|82b2j&fL!FvGI%r(!sMFPuc_SegK8i-PoMJhdZbQEI%D+NZb}kEs=v4{;dj@ z=~$Z>m=d1lc+xd>v;r_o1q|jb+kd14ERT{v)8B@3hQECM2333eG85SPtAA$zb$5kLc8DpfKHB%(J3(}v&g%jjnYHW;~BxThmTI^SAK6P zWcQ62043)A4qgr{kCK42QKi?l(u))6Nt)7Tl@UD06T8zCv}FOz43y{9!$HIn;l^~cJ4yXO?z^5EW4TKcw%0BV>5s`O0%CD zIslc%eU9Vu@{pE^*%6{uu@khRe~>ox57K&tG^R=FHp@fN6IsEtdw(5&0${-ucj+_$ z^0ljfaDU$6qnFavwo;9&)w;aivz>Xit>-cxYgh#!Uv2hNg9T7(*!23AJR6@8v>N!d zn>7_{fS8wl@hX5)WN~{@51`WU%ipx;+4zj0rvjfg{I=cg5E?hDyVM#8muw65+xPnn z5(zo95_rzDF2b|h_kX_wV3Ej@^+!({25l8py6eTe_~XZ#E~Gjn8ccPzRtweC>>v%IOY` z_$DR@Yx6y@Ecy=fgD#s4ut zn2fvr1(1}C7J`rw!mOIbn8fGw;@`~JmpuLl*YR=BZse0600000NkvXXu0mjfX^-cA diff --git a/base/src/main/res/mipmap-xhdpi/create_launcher.png b/base/src/main/res/mipmap-xhdpi/create_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..060f8d376fbbdca78c5337e6b9c765c71b202486 GIT binary patch literal 4333 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�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 literal 0 HcmV?d00001 diff --git a/base/src/main/res/mipmap-xxhdpi/open_note_launcher.png b/base/src/main/res/mipmap-xxhdpi/open_note_launcher.png index 8ef56f709f7efce6188103e356ee8848102d4d37..f487ed230e6962a37a9bba684525f9e92d9b4b2d 100644 GIT binary patch literal 8333 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 HHsSvVPD)Bq>bR%8^$P%C2B<2^ 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 KPNlYe Date: Sat, 19 Oct 2019 00:28:55 +0100 Subject: [PATCH 095/134] Adding additional logic to attempt fixing issue with trash clear --- base/src/main/java/com/maubis/scarlet/base/MainActivity.kt | 2 ++ .../com/maubis/scarlet/base/support/database/HouseKeeper.kt | 5 ++--- .../maubis/scarlet/base/support/database/HouseKeeperJob.kt | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 69d92a92..230ceb6c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -49,6 +49,7 @@ import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Compani import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount import com.maubis.scarlet.base.support.* +import com.maubis.scarlet.base.support.database.HouseKeeper import com.maubis.scarlet.base.support.database.HouseKeeperJob import com.maubis.scarlet.base.support.database.Migrator import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -506,6 +507,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { override fun onStop() { super.onStop() if (PermissionUtils().getStoragePermissionManager(this).hasAllPermissions()) { + HouseKeeper(this).removeOlderClips() NoteExporter().tryAutoExport() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt index 40902d67..bbc80274 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt @@ -32,10 +32,9 @@ class HouseKeeper(val context: Context) { } } - private fun removeOlderClips() { + fun removeOlderClips(deltaTimeMs: Long = 604800000L) { val notes = notesDb.database() - .getOldTrashedNotes( - Calendar.getInstance().timeInMillis - 1000 * 60 * 60 * 24 * 7) + .getOldTrashedNotes(Calendar.getInstance().timeInMillis - deltaTimeMs) for (note in notes) { note.delete(context) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeperJob.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeperJob.kt index e0d210b0..7ebfa955 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeperJob.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeperJob.kt @@ -10,7 +10,7 @@ class HouseKeeperJob : DailyJob() { fun schedule() { val builder = JobRequest.Builder(TAG).setRequiresDeviceIdle(true) - DailyJob.schedule(builder, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(7)) + schedule(builder, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(16)) } } From fa7db766588e1d08ce1066793327af7387639084 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 19 Oct 2019 00:55:41 +0100 Subject: [PATCH 096/134] Fixing link color on some backgrounds --- .../formats/recycler/FormatViewHolderBase.kt | 6 +++++- .../scarlet/base/support/ui/ThemeManager.kt | 16 ++++++++-------- base/src/main/res/values/colors.xml | 3 +++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index 7b1bfb54..f2389aa1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -48,24 +48,28 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView val hintTextColor: Int val theme = ApplicationBase.instance.themeController() val isLightBackground = ColorUtil.isLightColored(noteColor) + val linkColor: Int when { !useNoteColorAsBackground -> { secondaryTextColor = theme.get(ThemeColorType.SECONDARY_TEXT) tertiaryTextColor = theme.get(ThemeColorType.TERTIARY_TEXT) iconColor = theme.get(ThemeColorType.TOOLBAR_ICON) hintTextColor = theme.get(ThemeColorType.HINT_TEXT) + linkColor = theme.get(ThemeColorType.ACCENT_TEXT) } isLightBackground -> { secondaryTextColor = theme.get(context, Theme.LIGHT, ThemeColorType.SECONDARY_TEXT) tertiaryTextColor = theme.get(context, Theme.LIGHT, ThemeColorType.TERTIARY_TEXT) iconColor = theme.get(context, Theme.LIGHT, ThemeColorType.TOOLBAR_ICON) hintTextColor = theme.get(context, Theme.LIGHT, ThemeColorType.HINT_TEXT) + linkColor = ContextCompat.getColor(context, R.color.colorAccentYellowLight) } else -> { secondaryTextColor = theme.get(context, Theme.DARK, ThemeColorType.SECONDARY_TEXT) tertiaryTextColor = theme.get(context, Theme.DARK, ThemeColorType.TERTIARY_TEXT) iconColor = theme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) hintTextColor = theme.get(context, Theme.DARK, ThemeColorType.HINT_TEXT) + linkColor = ContextCompat.getColor(context, R.color.colorAccentYellowDark) } } val @@ -93,7 +97,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView tertiaryTextColor = tertiaryTextColor, iconColor = iconColor, hintTextColor = hintTextColor, - accentColor = theme.get(ThemeColorType.ACCENT_TEXT), + accentColor = linkColor, noteUUID = extra?.getString(INTENT_KEY_NOTE_ID) ?: "default") populate(data, config) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index e8a84a05..138eb4b0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -217,7 +217,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.app_theme_oceanic, toolbarIcon = R.color.white), @@ -229,7 +229,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.app_theme_violet, toolbarIcon = R.color.white), @@ -241,7 +241,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_yellow_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.app_theme_honeysuckle, toolbarIcon = R.color.white), @@ -253,7 +253,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.material_brown_900, toolbarIcon = R.color.white), @@ -265,7 +265,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.material_blue_grey_900, toolbarIcon = R.color.white), @@ -277,7 +277,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.material_grey_900, toolbarIcon = R.color.white), @@ -289,7 +289,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.material_grey_900, toolbarIcon = R.color.white), @@ -301,7 +301,7 @@ enum class Theme( tertiaryText = R.color.light_secondary_text, hintText = R.color.light_hint_text, disabledText = R.color.material_grey_200, - accentText = R.color.colorAccentDark, + accentText = R.color.material_pink_accent_100, sectionHeader = R.color.material_blue_grey_200, toolbarBackground = R.color.black, toolbarIcon = R.color.white), diff --git a/base/src/main/res/values/colors.xml b/base/src/main/res/values/colors.xml index b754efa5..58d425bd 100644 --- a/base/src/main/res/values/colors.xml +++ b/base/src/main/res/values/colors.xml @@ -6,8 +6,11 @@ #07575B #003B46 + #F52549 @color/material_pink_accent_200 + #FFFFCD + #505000 #C62828 From c9f822fef9f2371e1c4dbcce699f67c704b954bd Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 19 Oct 2019 01:12:21 +0100 Subject: [PATCH 097/134] Small Theme Fixes --- .../scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt | 1 + .../scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt | 2 +- base/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index a3ae91e8..8a68757d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -25,6 +25,7 @@ class InstallProUpsellBottomSheet : LithoBottomSheet() { GridSectionOptionItem(R.drawable.ic_action_lock, R.string.install_pro_sheet_app_lock, {}), GridSectionOptionItem(R.drawable.ic_action_day_mode, R.string.install_pro_sheet_app_themes, {}), GridSectionOptionItem(R.drawable.ic_title_white_48dp, R.string.install_pro_sheet_font_size, {}), + GridSectionOptionItem(R.drawable.ic_note_white_48dp, R.string.install_pro_sheet_note_options, {}), GridSectionOptionItem(R.drawable.ic_action_color, R.string.install_pro_sheet_viewer_bg, {}), GridSectionOptionItem(R.drawable.icon_widget, R.string.install_pro_sheet_widget_options, {})) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index 92a94cd3..c65d5740 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -83,7 +83,7 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { .textRes(R.string.theme_page_title) .marginDip(YogaEdge.HORIZONTAL, 0f)) - if (Build.VERSION.SDK_INT >= 28) { + if (Build.VERSION.SDK_INT >= 29) { column.child(OptionItemLayout.create(componentContext) .option(LithoOptionsItem( title = R.string.theme_use_system_theme, diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 48f75b98..8886afe8 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -236,6 +236,7 @@ Note Font Size Selection. Viewer Background Color. More Widget Options. + More Note Actions. Copy Block Text Actions From 24f8cbfa62f0c46f5eb1a0025fa059f873a7200b Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 24 Oct 2019 00:07:13 +0100 Subject: [PATCH 098/134] [ Fixing #150 ] Supporting Bullet and H3 Rendering --- .../scarlet/base/core/format/FormatType.kt | 3 ++ .../base/export/data/ExportableExtensions.kt | 6 ++- .../scarlet/base/note/MarkdownExtensions.kt | 8 ++++ .../base/note/formats/FormatControllerList.kt | 18 ++++++++ .../recycler/FormatBulletViewHolder.kt | 41 ++++++++++++++++++ base/src/main/res/drawable/icon_bullet_1.png | Bin 0 -> 498 bytes base/src/main/res/drawable/icon_bullet_2.png | Bin 0 -> 805 bytes base/src/main/res/drawable/icon_bullet_3.png | Bin 0 -> 198 bytes .../main/res/layout/item_format_bullet.xml | 39 +++++++++++++++++ 9 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatBulletViewHolder.kt create mode 100644 base/src/main/res/drawable/icon_bullet_1.png create mode 100644 base/src/main/res/drawable/icon_bullet_2.png create mode 100644 base/src/main/res/drawable/icon_bullet_3.png create mode 100644 base/src/main/res/layout/item_format_bullet.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt index 680b2150..72be2fd6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatType.kt @@ -4,6 +4,9 @@ enum class FormatType { TAG, TEXT, NUMBERED_LIST, + BULLET_1, + BULLET_2, + BULLET_3, IMAGE, HEADING,// HEADING_1 SUB_HEADING, // HEADING_2 diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt index 6964ed4a..3a5c5cb6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt @@ -26,6 +26,10 @@ fun Note.toExportedMarkdown(): String { val formatMarkdown = when (format.formatType) { FormatType.NUMBERED_LIST -> "- $text" FormatType.HEADING -> "# $text" + FormatType.HEADING_3 -> "### $text" + FormatType.BULLET_1 -> "- $text" + FormatType.BULLET_2 -> " - $text" + FormatType.BULLET_3 -> " - $text" FormatType.CHECKLIST_CHECKED -> "[x] $text" FormatType.CHECKLIST_UNCHECKED -> "[ ] $text" FormatType.SUB_HEADING -> "## $text" @@ -37,7 +41,7 @@ fun Note.toExportedMarkdown(): String { FormatType.TEXT -> text // NOTE: All the following states should never happen at this place - FormatType.HEADING_3 -> text + FormatType.TAG -> "" FormatType.EMPTY -> "" } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt index c104e51b..efde7ba3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt @@ -9,6 +9,10 @@ fun String.toInternalFormats(): List { return toInternalFormats(arrayOf( MarkdownSegmentType.HEADING_1, MarkdownSegmentType.HEADING_2, + MarkdownSegmentType.HEADING_3, + MarkdownSegmentType.BULLET_1, + MarkdownSegmentType.BULLET_2, + MarkdownSegmentType.BULLET_3, MarkdownSegmentType.CODE, MarkdownSegmentType.QUOTE, MarkdownSegmentType.CHECKLIST_UNCHECKED, @@ -32,6 +36,10 @@ fun String.toInternalFormats(whitelistedSegments: Array): L !isSegmentWhitelisted -> null segment.type() == MarkdownSegmentType.HEADING_1 -> Format(FormatType.HEADING, segment.strip()) segment.type() == MarkdownSegmentType.HEADING_2 -> Format(FormatType.SUB_HEADING, segment.strip()) + segment.type() == MarkdownSegmentType.HEADING_3 -> Format(FormatType.HEADING_3, segment.strip()) + segment.type() == MarkdownSegmentType.BULLET_1 -> Format(FormatType.BULLET_1, segment.strip()) + segment.type() == MarkdownSegmentType.BULLET_2 -> Format(FormatType.BULLET_2, segment.strip()) + segment.type() == MarkdownSegmentType.BULLET_3 -> Format(FormatType.BULLET_3, segment.strip()) segment.type() == MarkdownSegmentType.CODE -> Format(FormatType.CODE, segment.strip()) segment.type() == MarkdownSegmentType.QUOTE -> Format(FormatType.QUOTE, segment.strip()) segment.type() == MarkdownSegmentType.CHECKLIST_UNCHECKED -> Format(FormatType.CHECKLIST_UNCHECKED, segment.strip()) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt index 1c144cc5..14e56ee8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt @@ -51,6 +51,24 @@ fun getFormatControllerItems(): List> { .layoutFile(R.layout.item_format_code) .holderClass(FormatTextViewHolder::class.java) .build()) + list.add( + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.BULLET_1.ordinal) + .layoutFile(R.layout.item_format_bullet) + .holderClass(FormatBulletViewHolder::class.java) + .build()) + list.add( + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.BULLET_2.ordinal) + .layoutFile(R.layout.item_format_bullet) + .holderClass(FormatBulletViewHolder::class.java) + .build()) + list.add( + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.BULLET_3.ordinal) + .layoutFile(R.layout.item_format_bullet) + .holderClass(FormatBulletViewHolder::class.java) + .build()) list.add( MultiRecyclerViewControllerItem.Builder() .viewType(FormatType.CHECKLIST_CHECKED.ordinal) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatBulletViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatBulletViewHolder.kt new file mode 100644 index 00000000..fd923863 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatBulletViewHolder.kt @@ -0,0 +1,41 @@ +package com.maubis.scarlet.base.note.formats.recycler + +import android.content.Context +import android.view.View +import android.widget.ImageView +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.core.format.Format +import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.support.ui.visibility + +class FormatBulletViewHolder(context: Context, view: View) : FormatTextViewHolder(context, view) { + + private val firstMargin: View = root.findViewById(R.id.first_margin) + private val secondMargin: View = root.findViewById(R.id.second_margin) + private val icon: ImageView = root.findViewById(R.id.icon) + + override fun populate(data: Format, config: FormatViewHolderConfig) { + super.populate(data, config) + icon.setColorFilter(config.iconColor) + + when (data.formatType) { + FormatType.BULLET_1 -> { + icon.setImageResource(R.drawable.icon_bullet_1) + firstMargin.visibility = visibility(false) + secondMargin.visibility = visibility(false) + } + FormatType.BULLET_2 -> { + icon.setImageResource(R.drawable.icon_bullet_2) + firstMargin.visibility = visibility(false) + secondMargin.visibility = visibility(true) + } + FormatType.BULLET_3 -> { + icon.setImageResource(R.drawable.icon_bullet_3) + firstMargin.visibility = visibility(true) + secondMargin.visibility = visibility(true) + } + else -> { + } // Ignore other cases + } + } +} diff --git a/base/src/main/res/drawable/icon_bullet_1.png b/base/src/main/res/drawable/icon_bullet_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3b47c15c7e6195fab6082e2c8354c59d546f9edb GIT binary patch literal 498 zcmV@X+(P@ztnFwfMf zh%U+;5)eng0cFv^1be)a#4EduiCfHYP8#RTh&!zENm`#&UB#w>D`%Lyue+P@g>rVew-)mr&su0Ru^X03!>@?PW@ z<$qk^)0O>)qJ^h)2&(u}HQSB}L6I)nx)dckHqf?izUdi)0@rA{p^&U`9-yJclqAC} zFSypGEXh$Kz@8vu*n-qys;@-*y52mp4hfBNYJKE oC7V%;`@d0(2mMitTrQW^FM{l16n-Xv9RL6T07*qoM6N<$g5cBQ(f|Me literal 0 HcmV?d00001 diff --git a/base/src/main/res/drawable/icon_bullet_2.png b/base/src/main/res/drawable/icon_bullet_2.png new file mode 100644 index 0000000000000000000000000000000000000000..9f1c87e52f84bd6fdfc30b9737ace64d5a55ba9d GIT binary patch literal 805 zcmV+=1KRwFP)#HAp7fE9O&8T%acNc|)`$YKlLU;s%jkGnIs#_E!JXnFq~>cK@MQGR@|WrR`)0^A)z$)u<9yqa31xJ<`N>B^A2@ZoY51j=@K$# zVAWHca151AF$o#P$VMG^{@i~-jTZwLkNV=!>@o%Qk}_fdWq5&_B7egf)FLs2MW{1k zkK%kpbr3^nkNQY)u|pr!BQc1Fs6Jwc$*3J-5Iazl#SW`c6T~3KqgIO@&Z7E@LG*X^ zy(SISSPY^uDoyP02vu1OqOz;khcBoSVh|-zpT!OzP({TcilRP<9iE^ni9u9CJrO%( z{j-O<5h_dUa2hp03}OK4l-OYvYN8m#1k@^brDK~I#5Q-Oqt`zxu1`?C+_oR>#1Puq zv>y)DY~ep@&~iWv`_kbFQq<-PYMl8d-*H>SapPl97V3mJp%+%uBxFo;IVjpZ*zag2 zA)=Y;dd;T6%6s1d9gzwRr1X~io0 z@WpIA)j1}f+Grb3t)*;O0bVviWz#)xMx2UaWa39A!_39x!?}VNml;kWF^ksBAqgi* j%%MeI`_&>LA;EyJOh>B^dN$;=00000NkvXXu0mjfO)he} literal 0 HcmV?d00001 diff --git a/base/src/main/res/drawable/icon_bullet_3.png b/base/src/main/res/drawable/icon_bullet_3.png new file mode 100644 index 0000000000000000000000000000000000000000..00313445785f623e27cd00e8f8eff02888a05285 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!y`Cbp6ycdwUR{ro)O_n$~ckR>&YYeO$Kc!;U( P2k|^z{an^LB{Ts5lA1(z literal 0 HcmV?d00001 diff --git a/base/src/main/res/layout/item_format_bullet.xml b/base/src/main/res/layout/item_format_bullet.xml new file mode 100644 index 00000000..6497cf43 --- /dev/null +++ b/base/src/main/res/layout/item_format_bullet.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file From f888004f74ef01a36e114803de5946ac34647507 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 26 Oct 2019 12:12:12 +0100 Subject: [PATCH 099/134] #85 Fixing 24hr date format --- .../scarlet/base/config/ApplicationBase.kt | 4 +++ .../export/recycler/FileImportViewHolder.kt | 5 +-- .../base/export/support/NoteExporter.kt | 12 ++++--- .../scarlet/base/note/NoteExtensions.kt | 9 +++--- .../base/note/folder/FolderExtensions.kt | 5 +-- .../reminders/sheet/ReminderBottomSheet.kt | 6 ++-- .../base/support/utils/DateFormatUtils.kt | 31 +++++++++++++++++++ .../drive/GDrivePendingBottomSheet.kt | 7 +++-- 8 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index 372b4730..5c9de1dd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -6,12 +6,16 @@ import com.facebook.soloader.SoLoader import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator +import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.maybeThrow +import com.maubis.scarlet.base.support.utils.sDateFormat import java.lang.Exception +import java.lang.ref.WeakReference abstract class ApplicationBase : Application() { override fun onCreate() { super.onCreate() + sDateFormat = DateFormatUtils(this) SoLoader.init(this, false) try { JobManager.create(this).addJobCreator(ReminderJobCreator()) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt index e75bb1a1..03080e51 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt @@ -7,13 +7,14 @@ import android.os.Environment import android.view.View import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder -import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.sDateFormat import java.io.File class FileImportViewHolder(context: Context, root: View) @@ -54,7 +55,7 @@ class FileImportViewHolder(context: Context, root: View) } private fun getSubtitleText(file: File): String { - return DateFormatter.getDate("dd MMM yy \u00B7 hh:mm a", file.lastModified()) + return sDateFormat.readableFullTime(file.lastModified()) } private fun getMetaText(file: File): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt index 003f75d8..c0b7f7d0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt @@ -2,7 +2,6 @@ package com.maubis.scarlet.base.export.support import android.os.AsyncTask import android.os.Environment -import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.starter.util.FileManager import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase @@ -12,8 +11,8 @@ import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.export.data.* import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FILENAME import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FOLDER +import com.maubis.scarlet.base.support.utils.sDateFormat import java.io.File -import java.util.* const val KEY_NOTE_VERSION = "KEY_NOTE_VERSION" const val KEY_BACKUP_LOCATION = "KEY_BACKUP_LOCATION" @@ -79,17 +78,20 @@ class NoteExporter() { return@execute } - val exportFile = getOrCreateFileForExport(AUTO_BACKUP_FILENAME + " " + DateFormatter.getDate("dd_MMM_yyyy", Calendar.getInstance())) + val exportFile = getOrCreateFileForExport( + "$AUTO_BACKUP_FILENAME ${sDateFormat.getDateForBackup()}") if (exportFile === null) { return@execute } saveToFile(exportFile, getExportContent()) - ApplicationBase.instance.store().put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) + ApplicationBase.instance.store() + .put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) } } fun getOrCreateManualExportFile(): File? { - return getOrCreateFileForExport(NOTES_EXPORT_FILENAME + " " + DateFormatter.getDate("dd_MMM_yyyy HH_mm", Calendar.getInstance())) + return getOrCreateFileForExport( + "$NOTES_EXPORT_FILENAME ${sDateFormat.getTimestampForBackup()}") } fun getOrCreateFileForExport(filename: String): File? { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 14febbf1..caf1a62e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -1,9 +1,8 @@ package com.maubis.scarlet.base.note -import android.app.Activity import android.content.Context import android.content.Intent -import com.github.bijoysingh.starter.util.DateFormatter +import android.support.v4.app.ActivityOptionsCompat import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.MarkdownConfig @@ -27,10 +26,10 @@ import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Compa import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* import kotlin.collections.ArrayList -import android.support.v4.app.ActivityOptionsCompat - fun Note.log(): String { @@ -189,7 +188,7 @@ fun Note.getDisplayTime(): String { Calendar.getInstance().timeInMillis - time < 1000 * 60 * 60 * 2 -> "hh:mm aa" else -> "dd MMMM" } - return DateFormatter.getDate(format, time) + return sDateFormat.readableTime(format, time) } fun Note.getTagString(): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt index c616ebad..a0142ad9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt @@ -1,9 +1,10 @@ package com.maubis.scarlet.base.note.folder -import com.github.bijoysingh.starter.util.DateFormatter import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* fun Folder.saveIfUnique() { @@ -34,7 +35,7 @@ fun Folder.getDisplayTime(): String { Calendar.getInstance().timeInMillis - time < 1000 * 60 * 60 * 2 -> "hh:mm aa" else -> "dd MMMM" } - return DateFormatter.getDate(format, time) + return sDateFormat.readableTime(format, time) } /************************************************************************************** diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt index 59da8de9..02c81f65 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt @@ -21,6 +21,8 @@ import com.maubis.scarlet.base.support.sheets.LithoChooseOptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment +import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* @@ -198,8 +200,8 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { val date = Date(reminder.timestamp) reminderRepeat.setSubtitle(getReminderIntervalLabel(reminder.interval)) - reminderTime.setSubtitle(DateFormatter.getDate(DateFormatter.Formats.HH_MM_A.format, date)) - reminderDate.setSubtitle(DateFormatter.getDate(DateFormatter.Formats.DD_MMM_YYYY.format, date)) + reminderTime.setSubtitle(sDateFormat.readableTime(DateFormatter.Formats.HH_MM_A.format, reminder.timestamp)) + reminderDate.setSubtitle(sDateFormat.readableTime(DateFormatter.Formats.DD_MMM_YYYY.format, reminder.timestamp)) reminderDate.alpha = if (reminder.interval == ReminderInterval.ONCE) 1.0f else 0.5f } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt new file mode 100644 index 00000000..8b42231f --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt @@ -0,0 +1,31 @@ +package com.maubis.scarlet.base.support.utils + +import android.content.Context +import android.text.format.DateFormat +import com.github.bijoysingh.starter.util.DateFormatter +import java.util.* + +lateinit var sDateFormat: DateFormatUtils + +class DateFormatUtils(context: Context) { + private val is24HourFormat = DateFormat.is24HourFormat(context) + + fun readableFullTime(timestamp: Long): String = readableTime( + DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, + timestamp) + + fun readableTime(format: String, timestamp: Long): String { + val hourFormatSafe = when { + is24HourFormat -> format + .replace("a", "") + .replace("h", "H") + else -> format + } + return DateFormatter.getDate( + DateFormat.getBestDateTimePattern(Locale.getDefault(), hourFormatSafe), + timestamp) + } + + fun getDateForBackup(): String = DateFormatter.getDate("dd_MMM_yyyy", Calendar.getInstance()) + fun getTimestampForBackup(): String = DateFormatter.getDate("dd_MMM_yyyy HH_mm", Calendar.getInstance()) +} \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index f078b91a..d6ccbfac 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -13,7 +13,6 @@ import com.facebook.litho.widget.SolidColor import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge -import com.github.bijoysingh.starter.util.DateFormatter import com.google.gson.Gson import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase @@ -25,6 +24,8 @@ import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.sDateFormat import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -97,7 +98,7 @@ object PendingItemLayoutSpec { } val localUpdateTime = when { option.state.lastUpdateTimestamp == 0L -> "" - else -> DateFormatter.getDate(DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, option.state.lastUpdateTimestamp) + else -> sDateFormat.readableFullTime(option.state.lastUpdateTimestamp) } val remoteState = when { @@ -108,7 +109,7 @@ object PendingItemLayoutSpec { } val remoteUpdateTime = when { option.state.remoteUpdateTimestamp == 0L -> "" - else -> DateFormatter.getDate(DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, option.state.remoteUpdateTimestamp) + else -> sDateFormat.readableFullTime(option.state.remoteUpdateTimestamp) } val column = Column.create(context) From 082d7f05e87b086c3dbbce407cc4701dab493556 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 26 Oct 2019 14:28:00 +0100 Subject: [PATCH 100/134] #121 Adding delete options to folder --- .../sheet/CreateOrEditFolderBottomSheet.kt | 15 ++-- .../folder/sheet/DeleteFolderBottomSheet.kt | 84 ++++++++++++++++++ .../sheet/ThemeColorPickerBottomSheet.kt | 2 +- .../main/res/drawable/icon_delete_content.png | Bin 0 -> 320 bytes base/src/main/res/values/strings.xml | 10 ++- 5 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt create mode 100644 base/src/main/res/drawable/icon_delete_content.png diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt index 1e641527..320ddad5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt @@ -2,6 +2,7 @@ package com.maubis.scarlet.base.note.folder.sheet import android.app.Dialog import android.content.Context +import android.support.v7.app.AppCompatActivity import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -17,6 +18,7 @@ import com.maubis.scarlet.base.note.folder.delete import com.maubis.scarlet.base.note.folder.save import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.settings.view.ColorView +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -61,15 +63,14 @@ class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { sheetOnFolderListener(folder, !updated) dismiss() } + + val folderDeleteListener = sheetOnFolderListener removeBtn.visibility = if (folder.isUnsaved()) GONE else VISIBLE removeBtn.setOnClickListener { - folder.delete() - notesDb.getAll().filter { it.folder == folder.uuid }.forEach { - it.folder = "" - it.save(themedContext()) - } - - sheetOnFolderListener(folder, true) + openSheet(context as AppCompatActivity, DeleteFolderBottomSheet().apply { + selectedFolder = folder + sheetOnFolderListener = folderDeleteListener + }) dismiss() } enterFolder.setText(folder.title) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt new file mode 100644 index 00000000..68586b87 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt @@ -0,0 +1,84 @@ +package com.maubis.scarlet.base.note.folder.sheet + +import android.app.Dialog +import android.support.v7.app.AppCompatActivity +import com.facebook.litho.ComponentContext +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.database.room.folder.Folder +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.note.folder.delete +import com.maubis.scarlet.base.note.save +import com.maubis.scarlet.base.note.softDelete +import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem + +class DeleteFolderBottomSheet : LithoOptionBottomSheet() { + + var selectedFolder: Folder? = null + var sheetOnFolderListener: (folder: Folder, deleted: Boolean) -> Unit = { _, _ -> } + + override fun title(): Int = R.string.folder_delete_option_sheet_title + + override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { + val folder = selectedFolder + if (folder === null) { + dismiss() + return emptyList() + } + + val activity = context as AppCompatActivity + val options = ArrayList() + options.add(LithoOptionsItem( + title = R.string.folder_delete_option_sheet_remove_folder, + subtitle = R.string.folder_delete_option_sheet_remove_folder_details, + icon = R.drawable.icon_delete, + listener = { + folder.delete() + executeForFolderContent(folder) { + it.folder = "" + it.save(activity) + } + + sheetOnFolderListener(folder, true) + dismiss() + } + )) + options.add(LithoOptionsItem( + title = R.string.folder_delete_option_sheet_remove_folder_content, + subtitle = R.string.folder_delete_option_sheet_remove_folder_content_details, + icon = R.drawable.icon_delete_content, + listener = { + executeForFolderContent(folder) { + it.folder = "" + it.softDelete(activity) + } + + sheetOnFolderListener(folder, false) + dismiss() + } + )) + options.add(LithoOptionsItem( + title = R.string.folder_delete_option_sheet_remove_folder_and_content, + subtitle = R.string.folder_delete_option_sheet_remove_folder_and_content_details, + icon = R.drawable.ic_delete_permanently, + listener = { + folder.delete() + executeForFolderContent(folder) { + it.folder = "" + it.softDelete(activity) + } + + sheetOnFolderListener(folder, true) + dismiss() + } + )) + return options + } + + private fun executeForFolderContent(folder: Folder, lambda: (Note) -> Unit) { + CoreConfig.notesDb.getAll().filter { it.folder == folder.uuid }.forEach { + lambda(it) + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index c65d5740..034e8db1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -33,7 +33,7 @@ object ThemeColorPickerItemSpec { @Prop isDisabled: Boolean, @Prop isSelected: Boolean): Component { - var icon = RoundIcon.create(context) + val icon = RoundIcon.create(context) .showBorder(true) .iconSizeDip(64f) .iconPaddingDip(16f) diff --git a/base/src/main/res/drawable/icon_delete_content.png b/base/src/main/res/drawable/icon_delete_content.png new file mode 100644 index 0000000000000000000000000000000000000000..ad9a87dbdd00b614826da115e432ec969f47c61b GIT binary patch literal 320 zcmV-G0l)ry=svw*N;I7MI5onGixVsZyQrv$1(RbpR?%Up7CL$s(Xk(sxzP3-7 zB@fM9^OI|uEjg&?gx@%!+Psm+IA?e{W1Kt-!7Se9EChFWyR{IU;q5HD4w)Gvoa5!3 zam!gk2?@6Ok8Ki^m@mrNB*Z2az7LQ^LM@YLUg*X{k9lDQ53A;dXFNQa7v377%zl9~ z#(5)7P`~{Feer|J><=i59^~*}1t~~D3Q~}Q6r>;p1yGQJ6r>=^?GGrAA2euxz(D*U zn55Evfl4MJPS6t`p3-h$mBk~ye3l5cM85fSync all the notes, tags, etc to an external folder MaterialNotes/BACKUP.txt Export locked notes - Also backup the notes which are locked + Also backup the notes which are locked %s KB %s MB @@ -484,4 +484,12 @@ Use the light or dark theme from the system + Remove Options + Remove Notebook + Only Remove Notebook. Notes move outside folder. + Remove Notebook and Notes + Remove Notebook. All notes move to trash or are deleted. + Remove Notes + Keep Notebook but all notes move to trash or are deleted. + From 5af3328ec6bdb4988e93dff9f10bbc055ca1400d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 26 Oct 2019 23:37:37 +0100 Subject: [PATCH 101/134] [Share][Image] Fixing image shared to Scarlet --- base/src/main/AndroidManifest.xml | 21 ++- .../activity/OpenTextIntentOrFileActivity.kt | 46 ------ .../activity/ShareToScarletRouterActivity.kt | 138 ++++++++++++++++++ 3 files changed, 155 insertions(+), 50 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index 21bc57dd..8309489f 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -43,21 +43,34 @@ + - - + + + - + + + + + + - + + + diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 685d2088..5a9830f0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -2,7 +2,6 @@ package com.maubis.scarlet.base.main.activity import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle import android.text.Editable import android.text.SpannableString @@ -25,7 +24,6 @@ import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.bind import java.io.InputStreamReader @@ -84,7 +82,6 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { override fun afterTextChanged(text: Editable) { } - }) } @@ -114,21 +111,6 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { } fun handleIntent(): Boolean { - val hasDirectIntent = handleDirectSendText(intent) - if (hasDirectIntent) { - return false - } - - val hasSendIntent = handleSendText(intent) - if (hasSendIntent) { - val note = when (isCallerKeep()) { - true -> NoteBuilder().gen(titleText, NoteBuilder().genFromKeep(contentText)) - false -> NoteBuilder().gen(titleText, contentText) - } - note.save(this) - startActivity(ViewAdvancedNoteActivity.getIntent(this, note)) - return false - } val hasFileIntent = handleFileIntent(intent) if (hasFileIntent) { return true @@ -136,25 +118,6 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { return false } - fun handleSendText(intent: Intent): Boolean { - val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) - val sharedTitle = intent.getStringExtra(Intent.EXTRA_TITLE) - val sharedSubject = intent.getStringExtra(Intent.EXTRA_SUBJECT) - - titleText = sharedSubject ?: sharedTitle ?: "" - contentText = sharedText ?: "" - return sharedText != null - } - - fun handleDirectSendText(intent: Intent): Boolean { - val sharedText = intent.getStringExtra(INTENT_KEY_DIRECT_NOTES_TRANSFER) - if (sharedText === null || sharedText.isBlank()) { - return false - } - NoteImporter().gen(this, sharedText) - return true - } - fun handleFileIntent(intent: Intent): Boolean { val data = intent.data val lastPathSegment = data?.lastPathSegment @@ -173,15 +136,6 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { } } - fun isCallerKeep(): Boolean { - return when { - Build.VERSION.SDK_INT >= 22 && (referrer?.toString() ?: "").contains(KEEP_PACKAGE) -> true - callingPackage?.contains(KEEP_PACKAGE) ?: false -> true - (intent?.`package` ?: "").contains(KEEP_PACKAGE) -> true - else -> false - } - } - override fun notifyThemeChange() { setSystemTheme(); diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt new file mode 100644 index 00000000..88fa8882 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -0,0 +1,138 @@ +package com.maubis.scarlet.base.note.creation.activity + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.support.v7.app.AppCompatActivity +import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.core.format.Format +import com.maubis.scarlet.base.core.format.FormatBuilder +import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.core.note.NoteBuilder +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.database.room.note.Note +import com.maubis.scarlet.base.export.support.NoteImporter +import com.maubis.scarlet.base.main.activity.INTENT_KEY_DIRECT_NOTES_TRANSFER +import com.maubis.scarlet.base.main.activity.KEEP_PACKAGE +import com.maubis.scarlet.base.note.save +import java.io.File +import java.io.FileOutputStream + +class ShareToScarletRouterActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + try { + val hasDirectIntent = handleDirectSendText(intent) + if (hasDirectIntent) { + startActivity(Intent(this, MainActivity::class.java)) + return + } + + val note = handleSendText(intent) + if (note === null) { + return + } + startActivity(ViewAdvancedNoteActivity.getIntent(this, note)) + } finally { + finish() + } + } + + private fun handleSendText(intent: Intent): Note? { + val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "" + val sharedSubject = intent.getStringExtra(Intent.EXTRA_SUBJECT) + ?: intent.getStringExtra(Intent.EXTRA_TITLE) ?: "" + val sharedImages = when { + intent.action == Intent.ACTION_SEND -> handleSendImage(intent) + intent.action == Intent.ACTION_SEND_MULTIPLE -> handleSendMultipleImages(intent) + else -> emptyList() + } + if (sharedText.isBlank() && sharedSubject.isBlank() && sharedImages.isEmpty()) { + return null + } + + val note = when (isCallerKeep()) { + true -> NoteBuilder().gen(sharedSubject, NoteBuilder().genFromKeep(sharedText)) + false -> NoteBuilder().gen(sharedSubject, sharedText) + } + note.save(this) + + val images = emptyList().toMutableList() + for (uri in sharedImages) { + try { + val inputStream = this.contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream?.close() + + val temporaryImage = createTempFile() + saveToCache(temporaryImage, bitmap) + + images.add(noteImagesFolder.renameOrCopy(note, temporaryImage)) + temporaryImage.delete() + } catch (exception: Exception) { + } + } + val formats = note.getFormats().toMutableList() + for (image in images.reversed()) { + formats.add(0, Format(FormatType.IMAGE, image.name)) + } + note.description = FormatBuilder().getSmarterDescription(formats) + note.save(this) + return note + } + + private fun saveToCache(cacheFile: File, bitmap: Bitmap) { + val fOut = FileOutputStream(cacheFile) + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fOut) + fOut.flush() + fOut.close() + } + + private fun handleDirectSendText(intent: Intent): Boolean { + val sharedText = intent.getStringExtra(INTENT_KEY_DIRECT_NOTES_TRANSFER) + if (sharedText === null || sharedText.isBlank()) { + return false + } + NoteImporter().gen(this, sharedText) + return true + } + + private fun handleSendImage(intent: Intent): List { + val images = emptyList().toMutableList() + (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { + images.add(it) + } + return images + } + + private fun handleSendMultipleImages(intent: Intent): List { + val images = emptyList().toMutableList() + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.let { + for (parcelable in it) { + if (parcelable is Uri) { + images.add(parcelable) + } + } + } + return images + } + + private fun isCallerKeep(): Boolean { + return try { + when { + Build.VERSION.SDK_INT >= 22 && (referrer?.toString() ?: "").contains(KEEP_PACKAGE) -> true + callingPackage?.contains(KEEP_PACKAGE) ?: false -> true + (intent?.`package` ?: "").contains(KEEP_PACKAGE) -> true + else -> false + } + } catch (exception: Exception) { + false + } + } +} From 8ab8d3e3690d52f7a87cdbca2b60086c4da38e7d Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sat, 26 Oct 2019 23:56:16 +0100 Subject: [PATCH 102/134] #68 Fixing Image being deleted and adding again --- .../base/core/note/NoteSortingUtils.kt | 6 +++--- .../scarlet/base/database/room/note/Note.java | 2 +- .../activity/ShareToScarletRouterActivity.kt | 1 + .../formats/recycler/FormatImageViewHolder.kt | 21 +++++++++++-------- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt index 94a79180..cfe48f16 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteSortingUtils.kt @@ -61,12 +61,12 @@ fun sort(notes: List, sortingTechnique: SortingTechnique): List { val noteTagScore = it.getTagUUIDs().sumBy { tag -> tagCounterMap[tag] ?: 0 } - ComparablePair(ComparablePair(noteTagScore, it.tags), it.updateTimestamp) + ComparablePair(ComparablePair(noteTagScore, it.tags ?: ""), it.updateTimestamp) } } - else -> notes.sortedByDescending { note -> + SortingTechnique.NEWEST_FIRST -> notes.sortedByDescending { note -> if (note.pinned) Long.MAX_VALUE - else note.timestamp + else note.timestamp ?: note.updateTimestamp } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/database/room/note/Note.java b/base/src/main/java/com/maubis/scarlet/base/database/room/note/Note.java index 70ed043c..e7a91e27 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/room/note/Note.java +++ b/base/src/main/java/com/maubis/scarlet/base/database/room/note/Note.java @@ -29,7 +29,7 @@ public class Note { public boolean locked; - public String tags; + public String tags = ""; public long updateTimestamp; diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt index 88fa8882..150baa90 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -22,6 +22,7 @@ import com.maubis.scarlet.base.main.activity.KEEP_PACKAGE import com.maubis.scarlet.base.note.save import java.io.File import java.io.FileOutputStream +import java.util.* class ShareToScarletRouterActivity : AppCompatActivity() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index ddd101ba..5f099d80 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -82,15 +82,18 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase noImageMessage.setBackgroundColor(imageToolbarBg) val fileName = data.text - if (!fileName.isBlank()) { - val file = noteImagesFolder.getFile(config.noteUUID, data) - when (file.exists()) { - true -> populateFile(file) - false -> { - noImageMessage.setText(R.string.image_not_on_current_device) - noImageMessage.visibility = visibility(config.editable) - image.visibility = View.GONE - imageToolbar.visibility = View.GONE + when { + fileName.isBlank() -> image.visibility = View.GONE + else -> { + val file = noteImagesFolder.getFile(config.noteUUID, data) + when (file.exists()) { + true -> populateFile(file) + false -> { + noImageMessage.setText(R.string.image_not_on_current_device) + noImageMessage.visibility = visibility(config.editable) + image.visibility = View.GONE + imageToolbar.visibility = View.GONE + } } } } From 9bffa871f06b92545cc28c225d6a6156485bf466 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 27 Oct 2019 00:24:11 +0100 Subject: [PATCH 103/134] #76 Adding Share Image Support --- .../scarlet/base/note/NoteExtensions.kt | 21 ++++- .../note/actions/NoteOptionsBottomSheet.kt | 13 ++- .../activity/ShareToScarletRouterActivity.kt | 10 +-- .../sheet/SelectedNotesOptionsBottomSheet.kt | 79 +++++++++--------- .../scarlet/base/support/BitmapHelper.kt | 54 ++++++++++++ .../main/res/drawable/icon_share_image.png | Bin 0 -> 472 bytes base/src/main/res/values/strings.xml | 1 + 7 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt create mode 100644 base/src/main/res/drawable/icon_share_image.png diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index caf1a62e..b03b289a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -3,6 +3,7 @@ package com.maubis.scarlet.base.note import android.content.Context import android.content.Intent import android.support.v4.app.ActivityOptionsCompat +import android.widget.Toast import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.MarkdownConfig @@ -25,8 +26,8 @@ import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.sMarkdownEnabledHome import com.maubis.scarlet.base.settings.sheet.sInternalShowUUID import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled +import com.maubis.scarlet.base.support.BitmapHelper import com.maubis.scarlet.base.support.ui.ThemedActivity -import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* import kotlin.collections.ArrayList @@ -286,6 +287,24 @@ fun Note.share(context: Context) { ApplicationBase.instance.noteActions(this).share(context) } + +fun Note.hasImages(): Boolean { + val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } + return imageFormats.isNotEmpty() +} +fun Note.shareImages(context: Context) { + val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } + val bitmaps = imageFormats + .map { ApplicationBase.noteImagesFolder.getFile(uuid, it.text) } + .filter { it.exists() } + .map { BitmapHelper.loadFromFile(it) } + .filterNotNull() + when { + bitmaps.size == 1 -> BitmapHelper.send(context, bitmaps.first()) + bitmaps.size > 1 -> BitmapHelper.send(context, bitmaps) + } +} + fun Note.copy(context: Context) { ApplicationBase.instance.noteActions(this).copy(context) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 051be1ab..8fc68e78 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -117,7 +117,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { } } tagCardLayout.setOnClickListener { - com.maubis.scarlet.base.support.sheets.openSheet(activity, TagChooserBottomSheet().apply { + openSheet(activity, TagChooserBottomSheet().apply { this.note = note dismissListener = { activity.notifyTagsChanged(note) } }) @@ -331,6 +331,17 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() } )) + options.add(OptionsItem( + title = R.string.share_images, + subtitle = R.string.share_images, + icon = R.drawable.icon_share_image, + listener = View.OnClickListener { + note.shareImages(activity) + dismiss() + }, + visible = note.hasImages(), + invalid = activity.lockedContentIsHidden() && note.locked + )) options.add(OptionsItem( title = R.string.open_in_notification, subtitle = R.string.open_in_notification, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt index 150baa90..1e83f185 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -20,6 +20,7 @@ import com.maubis.scarlet.base.export.support.NoteImporter import com.maubis.scarlet.base.main.activity.INTENT_KEY_DIRECT_NOTES_TRANSFER import com.maubis.scarlet.base.main.activity.KEEP_PACKAGE import com.maubis.scarlet.base.note.save +import com.maubis.scarlet.base.support.BitmapHelper import java.io.File import java.io.FileOutputStream import java.util.* @@ -72,7 +73,7 @@ class ShareToScarletRouterActivity : AppCompatActivity() { inputStream?.close() val temporaryImage = createTempFile() - saveToCache(temporaryImage, bitmap) + BitmapHelper.saveToFile(temporaryImage, bitmap) images.add(noteImagesFolder.renameOrCopy(note, temporaryImage)) temporaryImage.delete() @@ -88,13 +89,6 @@ class ShareToScarletRouterActivity : AppCompatActivity() { return note } - private fun saveToCache(cacheFile: File, bitmap: Bitmap) { - val fOut = FileOutputStream(cacheFile) - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fOut) - fOut.flush() - fOut.close() - } - private fun handleDirectSendText(intent: Intent): Boolean { val sharedText = intent.getStringExtra(INTENT_KEY_DIRECT_NOTES_TRANSFER) if (sharedText === null || sharedText.isBlank()) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt index 31f9d1ae..f2fd794e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt @@ -159,32 +159,28 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { } )) - val allItemsPinned = !activity.getAllSelectedNotes().any { !it.pinned } - options.add(GridSectionOptionItem( - label = R.string.pin_note, - icon = R.drawable.ic_pin, - listener = lockAwareFunctionRunner(activity) { - activity.runNoteFunction { - it.pinned = true - it.save(activity) - } - activity.finish() - }, - visible = !allItemsPinned - )) + options.add(GridSectionOptionItem( - label = R.string.unpin_note, - icon = R.drawable.ic_pin, + label = R.string.folder_option_change_notebook, + icon = R.drawable.ic_folder, listener = lockAwareFunctionRunner(activity) { - activity.runNoteFunction { - it.pinned = false - it.save(activity) - } - activity.finish() - }, - visible = allItemsPinned + openSheet(activity, SelectedFolderChooseOptionsBottomSheet().apply { + this.dismissListener = {} + this.onActionListener = { folder, selectFolder -> + activity.runNoteFunction { + when (selectFolder) { + true -> it.folder = folder.uuid + false -> it.folder = "" + } + it.save(activity) + } + activity.finish() + } + }) + } )) + val allLocked = !activity.getAllSelectedNotes().any { !it.locked } options.add(GridSectionOptionItem( label = R.string.lock_note, @@ -220,25 +216,30 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { val activity = componentContext.androidContext as SelectNotesActivity val options = ArrayList() - + val allItemsPinned = !activity.getAllSelectedNotes().any { !it.pinned } options.add(GridSectionOptionItem( - label = R.string.folder_option_change_notebook, - icon = R.drawable.ic_folder, + label = R.string.pin_note, + icon = R.drawable.ic_pin, listener = lockAwareFunctionRunner(activity) { - openSheet(activity, SelectedFolderChooseOptionsBottomSheet().apply { - this.dismissListener = {} - this.onActionListener = { folder, selectFolder -> - activity.runNoteFunction { - when (selectFolder) { - true -> it.folder = folder.uuid - false -> it.folder = "" - } - it.save(activity) - } - activity.finish() - } - }) - } + activity.runNoteFunction { + it.pinned = true + it.save(activity) + } + activity.finish() + }, + visible = !allItemsPinned + )) + options.add(GridSectionOptionItem( + label = R.string.unpin_note, + icon = R.drawable.ic_pin, + listener = lockAwareFunctionRunner(activity) { + activity.runNoteFunction { + it.pinned = false + it.save(activity) + } + activity.finish() + }, + visible = allItemsPinned )) options.add(GridSectionOptionItem( diff --git a/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt b/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt new file mode 100644 index 00000000..ceb10dc2 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt @@ -0,0 +1,54 @@ +package com.maubis.scarlet.base.support + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.provider.MediaStore +import java.io.File +import java.io.FileOutputStream + +object BitmapHelper { + fun send(context: Context, bitmap: Bitmap) { + val path = MediaStore.Images.Media.insertImage( + context.contentResolver, bitmap, "Scarlet Image", "Scarlet Image") + val uri = Uri.parse(path) + + val intent = Intent(Intent.ACTION_SEND) + intent.type = "image/jpeg" + intent.putExtra(Intent.EXTRA_STREAM, uri) + context.startActivity(Intent.createChooser(intent, "Share Image")) + } + + fun send(context: Context, bitmaps: List) { + val fileUris = ArrayList() + bitmaps + .mapIndexed { index, bitmap -> + MediaStore.Images.Media.insertImage( + context.contentResolver, bitmap, "Scarlet Image ($index)", "Scarlet Image ($index)") + }.map { + Uri.parse(it) + }.forEach { fileUris.add(it) } + val intent = Intent(Intent.ACTION_SEND_MULTIPLE) + intent.type = "image/jpeg" + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris) + context.startActivity(Intent.createChooser(intent, "Share Images")) + } + + fun loadFromFile(cacheFile: File): Bitmap? { + if (cacheFile.exists()) { + val options = BitmapFactory.Options() + options.inPreferredConfig = Bitmap.Config.ARGB_8888 + return BitmapFactory.decodeFile(cacheFile.absolutePath, options) + } + return null + } + + fun saveToFile(cacheFile: File, bitmap: Bitmap) { + val fOut = FileOutputStream(cacheFile) + bitmap.compress(Bitmap.CompressFormat.PNG, 90, fOut) + fOut.flush() + fOut.close() + } +} \ No newline at end of file diff --git a/base/src/main/res/drawable/icon_share_image.png b/base/src/main/res/drawable/icon_share_image.png new file mode 100644 index 0000000000000000000000000000000000000000..69acba268bd0a2cce79ee09e6a8393502b4d29dc GIT binary patch literal 472 zcmV;}0Vn>6P)NklDuqRyipnftG+u0099C2$2vVQHWAR0L4HdfSIoC4QJ8 z3bW#eG*P$~KYS1cSNsqW6BK38ie^C~OjFV+$io<4It6(c!PP0q!yvCZ1$pS@CE6gD z6x&jU9v-6!dS;P+EU7>z8P!3TOb`=av~i&>$YDyV!sb*>&;fC&3C-lg58A_6=-6BVZ2AmV6hKj2OUcp`j}&vG@q5zG;;)D z2bJiTUyO3bx8F1^oIyxIm7g=?WP+w?BUH)+7&VJM>kni%94;^*esDp zg~FeQb1afL=)fU~C9I>94dES~ya^6EarAp-BCJtF2St+7@C?H+4C4p4-1Jn5BY5oq O0000Edit Note Create Notification + Share Photos Open In Popup Delete Permanently Copy Note From 5605ef9543a81d09d4b1faac777c91b09f9d289b Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 27 Oct 2019 22:34:13 +0000 Subject: [PATCH 104/134] [Cleanup] Unifying OS Version Checks --- app/build.gradle | 4 +-- .../base/core/note/MaterialNoteActor.kt | 3 ++- .../base/export/support/ExternalFolderSync.kt | 4 ++- .../base/main/sheets/WhatsNewBottomSheet.kt | 25 +++++++----------- .../main/specs/MainActivityBottomBarSpec.kt | 3 ++- .../note/actions/NoteOptionsBottomSheet.kt | 5 ++-- .../note/actions/TextToSpeechBottomSheet.kt | 11 ++++---- .../activity/ShareToScarletRouterActivity.kt | 3 ++- .../creation/activity/ViewNoteActivity.kt | 9 +++---- .../base/notification/NotificationHandler.kt | 6 +++-- .../sheet/ThemeColorPickerBottomSheet.kt | 3 ++- .../scarlet/base/support/ShortcutUtils.kt | 9 ++++--- .../scarlet/base/support/ui/ThemeManager.kt | 3 ++- .../scarlet/base/support/ui/ThemedActivity.kt | 5 ++-- .../base/support/utils/AppVersionUtils.kt | 6 ++--- .../base/support/utils/OsVersionUtils.kt | 23 ++++++++++++++++ base/src/main/res/drawable/icon_languages.png | Bin 0 -> 848 bytes base/src/main/res/values/strings.xml | 10 ++++--- build.gradle | 4 +-- 19 files changed, 83 insertions(+), 53 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt create mode 100644 base/src/main/res/drawable/icon_languages.png diff --git a/app/build.gradle b/app/build.gradle index 18342315..20261c50 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.bijoysingh.quicknote" minSdkVersion 21 targetSdkVersion 28 - versionCode 147 - versionName '7.1.1' + versionCode 148 + versionName '7.2.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index 24ee2cd3..644f6f64 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -20,6 +20,7 @@ import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.service.FloatingNoteService +import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.widget.AllNotesWidgetProvider.Companion.notifyAllChanged import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -115,7 +116,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { WidgetConfigureActivity.notifyNoteChange(context, note) notifyAllChanged(context) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? - if (Build.VERSION.SDK_INT >= 23 && notificationManager != null) { + if (OsVersionUtils.canExtractActiveNotifications() && notificationManager != null) { for (notification in notificationManager.activeNotifications) { if (notification.id == note.uid) { val handler = NotificationHandler(context) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index c2e07016..42194a67 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -14,6 +14,7 @@ import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.export.data.ExportableTag import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FOLDER +import com.maubis.scarlet.base.support.utils.OsVersionUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -40,7 +41,8 @@ var sFolderSyncBackupLocked: Boolean object ExternalFolderSync { fun hasPermission(context: Context): Boolean { - return !(Build.VERSION.SDK_INT >= 23 && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) + return !(OsVersionUtils.requiresPermissions() + && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) } fun enable(context: Context, enabled: Boolean) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 70e36d7a..24c91fe2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -17,17 +17,21 @@ import com.maubis.scarlet.base.support.specs.GridSectionOptionItem import com.maubis.scarlet.base.support.specs.GridSectionView import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.utils.FlavorUtils +import com.maubis.scarlet.base.support.utils.OsVersionUtils + +const val WHATS_NEW_SHEET_INDEX = 11 class WhatsNewBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val options = listOf( if (FlavorUtils.isOpenSource()) null else GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), - if (FlavorUtils.isOpenSource()) null else GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_photo_sync, {}), - GridSectionOptionItem(R.drawable.ic_action_lock, R.string.whats_new_sheet_app_lock, {}), - GridSectionOptionItem(R.drawable.ic_action_select, R.string.whats_new_sheet_selection, {}), - GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_widget, {}), - GridSectionOptionItem(R.drawable.ic_image_gallery, R.string.whats_new_sheet_more_languages, {})) + GridSectionOptionItem(R.drawable.icon_share_image, R.string.whats_new_sheet_photo_share, {}), + GridSectionOptionItem(R.drawable.ic_action_color, R.string.whats_new_sheet_note_color, {}), + GridSectionOptionItem(R.drawable.icon_languages, R.string.whats_new_sheet_more_languages, {}), + if (!OsVersionUtils.canAddLauncherShortcuts()) null else GridSectionOptionItem(R.drawable.icon_shortcut, R.string.whats_new_sheet_launcher_shortcuts, {}), + GridSectionOptionItem(R.drawable.ic_markdown_logo, R.string.whats_new_sheet_markdown_improvements, {}), + GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_ui_improvements, {})) val component = Column.create(componentContext) .widthPercent(100f) @@ -39,7 +43,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) - .text(WHATS_NEW_DETAILS_SUBTITLE) + .textRes(R.string.whats_new_sheet_subtitle) .typeface(FONT_MONSERRAT) .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) .child(GridSectionView.create(componentContext) @@ -56,13 +60,4 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } - - companion object { - val WHATS_NEW_UID = 11 - - val WHATS_NEW_DETAILS_SUBTITLE = "A lot has changed in this update, here is a summary of those changes." - val WHATS_NEW_DETAILS_NEW_FEATURES_TITLE = "New Features" - - - } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 93e35070..c568cdbe 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -36,6 +36,7 @@ import com.maubis.scarlet.base.support.specs.bottomBarCard import com.maubis.scarlet.base.support.specs.bottomBarRoundIcon import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.OsVersionUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -78,7 +79,7 @@ object MainActivityBottomBarSpec { .iconRes(R.drawable.icon_add_note) .isLongClickEnabled(true) .onLongClick { - if (Build.VERSION.SDK_INT < 26) { + if (!OsVersionUtils.canAddLauncherShortcuts()) { return@onLongClick } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 8fc68e78..4f390832 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -43,6 +43,7 @@ import com.maubis.scarlet.base.support.sheets.GridBottomSheetBase import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.FlavorUtils +import com.maubis.scarlet.base.support.utils.OsVersionUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async @@ -370,7 +371,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { subtitle = R.string.pin_to_launcher, icon = R.drawable.icon_shortcut, listener = View.OnClickListener { - if (!FlavorUtils.isLite() && Build.VERSION.SDK_INT >= 26) { + if (!FlavorUtils.isLite() && OsVersionUtils.canAddLauncherShortcuts()) { var title = note.getTitleForSharing() if (title.isBlank()) { title = note.getFullText().split("\n").firstOrNull() ?: "Note" @@ -388,7 +389,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { } openSheet(activity, InstallProUpsellBottomSheet()) }, - visible = Build.VERSION.SDK_INT >= 26, + visible = OsVersionUtils.canAddLauncherShortcuts(), invalid = activity.lockedContentIsHidden() && note.locked )) options.add(OptionsItem( diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index a4844e08..3eff851a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -1,7 +1,6 @@ package com.maubis.scarlet.base.note.actions import android.app.Dialog -import android.os.Build import android.speech.tts.TextToSpeech import android.widget.ImageView import android.widget.TextView @@ -13,6 +12,7 @@ import com.maubis.scarlet.base.note.getFullText import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment +import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.removeMarkdownHeaders fun Note.getTextToSpeechText(): String { @@ -63,11 +63,10 @@ class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { makeBackgroundTransparent(dialog, R.id.root_layout) } - fun speak(note: Note) { - if (Build.VERSION.SDK_INT >= 21) { - textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null, "NOTE") - } else { - textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null) + private fun speak(note: Note) { + when (OsVersionUtils.requiresTTSUtteranceId()) { + true -> textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null, "NOTE") + false -> textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt index 1e83f185..08f6791f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -21,6 +21,7 @@ import com.maubis.scarlet.base.main.activity.INTENT_KEY_DIRECT_NOTES_TRANSFER import com.maubis.scarlet.base.main.activity.KEEP_PACKAGE import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.support.BitmapHelper +import com.maubis.scarlet.base.support.utils.OsVersionUtils import java.io.File import java.io.FileOutputStream import java.util.* @@ -121,7 +122,7 @@ class ShareToScarletRouterActivity : AppCompatActivity() { private fun isCallerKeep(): Boolean { return try { when { - Build.VERSION.SDK_INT >= 22 && (referrer?.toString() ?: "").contains(KEEP_PACKAGE) -> true + OsVersionUtils.canExtractReferrer() && (referrer?.toString() ?: "").contains(KEEP_PACKAGE) -> true callingPackage?.contains(KEEP_PACKAGE) ?: false -> true (intent?.`package` ?: "").contains(KEEP_PACKAGE) -> true else -> false diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index b0047e58..2e14815b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -4,7 +4,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Color -import android.os.Build import android.os.Bundle import android.support.v7.widget.RecyclerView import android.view.View @@ -139,14 +138,12 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit } private fun startDistractionFreeMode() { - var uiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + val uiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN) - if (Build.VERSION.SDK_INT >= 19) { - uiVisibility = (uiVisibility or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) - } + or View.SYSTEM_UI_FLAG_FULLSCREEN + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) window.decorView.systemUiVisibility = uiVisibility } diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt index 8bd514bc..c9680ac5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt @@ -4,7 +4,6 @@ import android.app.* import android.content.Context import android.content.Context.NOTIFICATION_SERVICE import android.content.Intent -import android.os.Build import android.support.v4.app.NotificationCompat import android.view.View.GONE import android.view.View.VISIBLE @@ -22,6 +21,7 @@ import com.maubis.scarlet.base.note.getTextForSharing import com.maubis.scarlet.base.note.getTitleForSharing import com.maubis.scarlet.base.support.INTENT_KEY_ACTION import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.utils.OsVersionUtils const val REQUEST_CODE_BASE = 3200; const val REQUEST_CODE_MULTIPLIER = 250; @@ -65,13 +65,15 @@ class NotificationHandler(val context: Context) { } private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT < 26) { + if (!OsVersionUtils.canAddNotificationChannels()) { return } + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? if (manager === null) { return } + val channel = NotificationChannel( NOTE_NOTIFICATION_CHANNEL_ID, context.getString(R.string.notification_channel_label), diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index 034e8db1..c1be819e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -24,6 +24,7 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.sAutomaticTheme import com.maubis.scarlet.base.support.ui.setThemeFromSystem import com.maubis.scarlet.base.support.utils.FlavorUtils +import com.maubis.scarlet.base.support.utils.OsVersionUtils @LayoutSpec object ThemeColorPickerItemSpec { @@ -83,7 +84,7 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { .textRes(R.string.theme_page_title) .marginDip(YogaEdge.HORIZONTAL, 0f)) - if (Build.VERSION.SDK_INT >= 29) { + if (OsVersionUtils.canUseSystemTheme()) { column.child(OptionItemLayout.create(componentContext) .option(LithoOptionsItem( title = R.string.theme_use_system_theme, diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt index af48bae8..bf3ceb5a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt @@ -5,16 +5,20 @@ import android.content.Context import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.os.Build +import com.maubis.scarlet.base.support.utils.OsVersionUtils import java.util.* fun addShortcut(context: Context, shortcut: ShortcutInfo) { - if (Build.VERSION.SDK_INT < 26) { + if (!OsVersionUtils.canAddLauncherShortcuts()) { return } val shortcutManager = context.getSystemService(ShortcutManager::class.java) - shortcutManager.dynamicShortcuts = Arrays.asList(shortcut) + if (shortcutManager === null) { + return + } + shortcutManager.dynamicShortcuts = listOf(shortcut) if (shortcutManager.isRequestPinShortcutSupported) { val pinShortcutInfo = ShortcutInfo.Builder(context, shortcut.id).build() val pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo) @@ -23,5 +27,4 @@ fun addShortcut(context: Context, shortcut: ShortcutInfo) { pinnedShortcutCallbackIntent, 0) shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) } - } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index 138eb4b0..c18b49fd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -9,6 +9,7 @@ import com.github.bijoysingh.starter.util.DimensionManager import com.maubis.markdown.MarkdownConfig import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.throwOrReturn import java.lang.ref.WeakReference @@ -107,7 +108,7 @@ class ThemeManager : IThemeManager { val colorResource = when (type) { ThemeColorType.BACKGROUND -> theme.background ThemeColorType.STATUS_BAR -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) theme.background + if (OsVersionUtils.canSetStatusBarTheme()) theme.background else theme.statusBarColorFallback ?: theme.background } ThemeColorType.PRIMARY_TEXT -> theme.primaryText diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index b97b0640..219ae310 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -13,6 +13,7 @@ import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.settings.sheet.sInternalEnableFullScreen +import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.maybeThrow abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { @@ -65,13 +66,13 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { } fun setStatusBarColor(color: Int) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (OsVersionUtils.canSetStatusBarColor()) { window.statusBarColor = color } } fun setStatusBarTextColor() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (OsVersionUtils.canSetStatusBarTheme()) { val view = window.decorView var flags = view.systemUiVisibility flags = when (ApplicationBase.instance.themeController().isNightTheme()) { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt index c27eb0cb..ee51800b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt @@ -3,7 +3,7 @@ package com.maubis.scarlet.base.support.utils import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb -import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet.Companion.WHATS_NEW_UID +import com.maubis.scarlet.base.main.sheets.WHATS_NEW_SHEET_INDEX import java.util.* const val KEY_LAST_KNOWN_APP_VERSION = "KEY_LAST_KNOWN_APP_VERSION" @@ -30,7 +30,7 @@ fun getLastUsedAppVersionCode(): Int { fun shouldShowWhatsNewSheet(): Boolean { val lastShownWhatsNew = ApplicationBase.instance.store().get(KEY_LAST_SHOWN_WHATS_NEW, 0) - if (lastShownWhatsNew >= WHATS_NEW_UID) { + if (lastShownWhatsNew >= WHATS_NEW_SHEET_INDEX) { // Already shown the latest return false } @@ -38,7 +38,7 @@ fun shouldShowWhatsNewSheet(): Boolean { val lastUsedAppVersion = getLastUsedAppVersionCode() // Update the values independent of the decision - ApplicationBase.instance.store().put(KEY_LAST_SHOWN_WHATS_NEW, WHATS_NEW_UID) + ApplicationBase.instance.store().put(KEY_LAST_SHOWN_WHATS_NEW, WHATS_NEW_SHEET_INDEX) ApplicationBase.instance.store().put(KEY_LAST_KNOWN_APP_VERSION, getCurrentVersionCode()) // New users don't need to see the whats new screen diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt new file mode 100644 index 00000000..373e0d73 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt @@ -0,0 +1,23 @@ +package com.maubis.scarlet.base.support.utils + +import android.os.Build + +object OsVersionUtils { + fun canSetStatusBarColor() = Build.VERSION.SDK_INT >= 21 + + fun requiresTTSUtteranceId() = Build.VERSION.SDK_INT >= 21 + + fun canExtractReferrer() = Build.VERSION.SDK_INT >= 22 + + fun requiresPermissions() = Build.VERSION.SDK_INT >= 23 + + fun canSetStatusBarTheme() = Build.VERSION.SDK_INT >= 23 + + fun canExtractActiveNotifications() = Build.VERSION.SDK_INT >= 23 + + fun canAddLauncherShortcuts() = Build.VERSION.SDK_INT >= 26 + + fun canAddNotificationChannels() = Build.VERSION.SDK_INT >= 26 + + fun canUseSystemTheme() = Build.VERSION.SDK_INT >= 29 +} diff --git a/base/src/main/res/drawable/icon_languages.png b/base/src/main/res/drawable/icon_languages.png new file mode 100644 index 0000000000000000000000000000000000000000..f825ccad5adc63fd5d1af8e88d324db18392bada GIT binary patch literal 848 zcmV-W1F!svP)40*Y+c*7jo{k0ZQHhO+ctu0+gR@)o3-(-rt_y~tLJph>6$Z1&J(l0q^f?j zii(Q*8cMj7R$94~67>aKP2?K&1&kmvTzvrs6+|(Ry`45fXyYPE@_9QUTq|FUAf(~) zMJpk-$`{uV(lzqOMGPf`pDbr{(?*ke2fJaWu$i?B+Aw=40p*;+cZ}gX zPN7O!;2q2`7Lp`t_zCl-67ekN8!D7P7GWIbTPi5!P0R@9Qvwbn?zHTW4IbiN%rv%D zB7P_CKV^fPNpgu2u?}*+^1|XAA{-YPwJJ33WhVMF;puXY(Q75 zs2l8#+rG*IRlG-6A5fEO&W1-Y?99Pl>EgKY9cB&1u(o$KVi`K}inBuZpX7JUTInZfbELd*BkA3$ z+!&KroKo6J!J``SlYbn;4*1zcxv(XZ0hr1$^2Q7Jc}2OgA&>DSo6Cn)AU~66RyZ7w zzmpXXUtv~AxgLF|P*@@TXZSr9Jq)hHoT6@V7Uo8U5UW5_ay3roWm@20$!v>x-LoU? zpzLv%-|)qLm`I literal 0 HcmV?d00001 diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 29f1557a..defbbcca 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -224,12 +224,14 @@ Know what is new in the recent updates of the app Translate + A lot has changed in this update, here is a summary of those changes. Sign In with Drive. More Privacy. - Now Photos are Synced too. - App Lock and Single Unlock. + Share Photos. To and From. + More Note Colors. + Launcher Shortcuts. More Languages Supported. - More Widget Settings. - Easier Note Selection. + H3 and Bullet Support. + UI Improvements. Faster Updates and Requests. App Lock and Single Unlock. diff --git a/build.gradle b/build.gradle index 8070b3ba..f09dfa67 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { ext.android_support_version = '28.0.0' // NOTE: Keep this in sync with the build.gradle for app/ - ext.appconfig_version_code = 147 - ext.appconfig_version = '7.1.1' + ext.appconfig_version_code = 148 + ext.appconfig_version = '7.2.0' ext.appconfig_min_os_version = 21 ext.appconfig_target_os_version = 28 ext.appconfig_build_tool_version = '28.0.3' From 11da2d7acd77ccb2a0cad92934352443e355b5ac Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 27 Oct 2019 22:46:59 +0000 Subject: [PATCH 105/134] Deleting the list item using a close button --- .../note/formats/recycler/FormatListViewHolder.kt | 9 +++++++++ base/src/main/res/layout/item_format_list.xml | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt index e3c1216d..57373704 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt @@ -9,11 +9,13 @@ import android.widget.ImageView import com.maubis.scarlet.base.R import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType +import com.maubis.scarlet.base.support.ui.visibility import com.maubis.scarlet.base.support.utils.getEditorActionListener class FormatListViewHolder(context: Context, view: View) : FormatTextViewHolder(context, view) { private val icon: ImageView = root.findViewById(R.id.icon) + private val close: ImageView = root.findViewById(R.id.close) init { edit.setOnEditorActionListener(getEditorActionListener( @@ -46,6 +48,13 @@ class FormatListViewHolder(context: Context, view: View) : FormatTextViewHolder( } // Ignore other cases } + close.visibility = visibility(config.editable) + close.setColorFilter(config.iconColor) + close.alpha = 0.8f + close.setOnClickListener { + activity.deleteFormat(format) + } + itemView.setOnClickListener { if (!config.editable) { activity.setFormatChecked(data, data.formatType != FormatType.CHECKLIST_CHECKED) diff --git a/base/src/main/res/layout/item_format_list.xml b/base/src/main/res/layout/item_format_list.xml index 8b4aaa90..eb2b1c65 100644 --- a/base/src/main/res/layout/item_format_list.xml +++ b/base/src/main/res/layout/item_format_list.xml @@ -8,8 +8,8 @@ android:id="@+id/icon" android:layout_width="@dimen/icon_size_normal" android:layout_height="@dimen/icon_size_normal" - android:layout_marginStart="@dimen/spacing_large" android:layout_gravity="center_vertical" + android:layout_marginStart="@dimen/spacing_large" android:src="@drawable/ic_check_box_outline_blank_white_24dp" android:tint="@color/material_blue_grey_500" /> @@ -22,4 +22,16 @@ + + + \ No newline at end of file From 0226b3b0f76edbef3624fff3b2e242add40af190 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 27 Oct 2019 22:58:36 +0000 Subject: [PATCH 106/134] [Fix] Fixing bug in Undo / Redo save --- .../note/creation/activity/CreateNoteActivity.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index 0158b1eb..fb751672 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -45,8 +45,8 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { private var historyIndex = 0 private var historySize = 0L - - val history: MutableList = emptyList().toMutableList() + private var historyModified = false + private val history: MutableList = emptyList().toMutableList() override val editModeValue: Boolean get() = true @@ -193,11 +193,11 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { currentNote.description = FormatBuilder().getSmarterDescription(formats) // Ignore update if nothing changed. It allows for one undo per few seconds - if (currentNote.isEqual(vLastNoteInstance)) { - return + when { + !historyModified && currentNote.isEqual(vLastNoteInstance) -> return + !historyModified -> addNoteToHistory(NoteBuilder().copy(currentNote)) + else -> historyModified = false } - - addNoteToHistory(NoteBuilder().copy(currentNote)) currentNote.updateTimestamp = Calendar.getInstance().timeInMillis maybeSaveNote(false) } @@ -313,12 +313,14 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { historyIndex = if (historyIndex == 0) 0 else (historyIndex - 1) note = NoteBuilder().copy(history.get(historyIndex)) setNote() + historyModified = true } false -> { val maxHistoryIndex = history.size - 1 historyIndex = if (historyIndex == maxHistoryIndex) maxHistoryIndex else (historyIndex + 1) note = NoteBuilder().copy(history.get(historyIndex)) setNote() + historyModified = true } } } From 7f832db15b9ddfe62199f8077aae455c4c92e54a Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 27 Oct 2019 23:04:30 +0000 Subject: [PATCH 107/134] #158 Fixing H3 in List View --- .../com/maubis/scarlet/base/note/NoteExtensions.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index b03b289a..f950ad5e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -3,7 +3,6 @@ package com.maubis.scarlet.base.note import android.content.Context import android.content.Intent import android.support.v4.app.ActivityOptionsCompat -import android.widget.Toast import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.MarkdownConfig @@ -82,6 +81,12 @@ internal fun markdownFormatForList(text: String): CharSequence { .bold(s, e) true } + MarkdownType.HEADING_3 -> { + spannable.relativeSize(1.0f, s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) + true + } MarkdownType.CHECKLIST_CHECKED -> { spannable.strike(s, e) true @@ -98,9 +103,9 @@ fun Note.getTitleForSharing(): String { return "" } val format = formats.first() + val headingFormats = listOf(FormatType.HEADING, FormatType.SUB_HEADING, FormatType.HEADING_3) return when { - format.formatType === FormatType.HEADING -> format.text - format.formatType === FormatType.SUB_HEADING -> format.text + headingFormats.contains(format.formatType) -> format.text else -> "" } } @@ -292,6 +297,7 @@ fun Note.hasImages(): Boolean { val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } return imageFormats.isNotEmpty() } + fun Note.shareImages(context: Context) { val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } val bitmaps = imageFormats From 38f1b7656a23c66909e085828d70c3780252967b Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 30 Oct 2019 23:02:42 +0000 Subject: [PATCH 108/134] [Refactor][1] Moving some configs out to static --- .../com/bijoysingh/quicknote/MaterialNotes.kt | 1 - .../java/com/maubis/scarlet/base/MainActivity.kt | 4 ++-- .../maubis/scarlet/base/MainActivityExtensions.kt | 2 +- .../maubis/scarlet/base/config/ApplicationBase.kt | 15 +++++++++++++-- .../com/maubis/scarlet/base/config/CoreConfig.kt | 6 ------ .../scarlet/base/config/MaterialNoteConfig.kt | 9 --------- .../scarlet/base/core/note/MaterialNoteActor.kt | 3 +-- .../scarlet/base/core/note/NoteExtensions.kt | 1 - .../maubis/scarlet/base/core/note/NoteImage.kt | 12 ++++++------ .../base/export/activity/ImportNoteActivity.kt | 5 ++--- .../base/export/recycler/FileImportViewHolder.kt | 5 ++--- .../base/export/sheet/ExportNotesBottomSheet.kt | 2 +- .../export/sheet/ExternalFolderSyncBottomSheet.kt | 6 +++--- .../base/export/sheet/PermissionBottomSheet.kt | 2 +- .../base/export/support/ExternalFolderSync.kt | 1 - .../main/activity/OpenTextIntentOrFileActivity.kt | 6 +++--- .../base/main/activity/WidgetConfigureActivity.kt | 1 - .../main/recycler/ToolbarMainRecyclerHolder.kt | 4 ++-- .../scarlet/base/main/sheets/AlertBottomSheet.kt | 2 +- .../base/main/sheets/ExceptionBottomSheet.kt | 6 +++--- .../base/main/sheets/HomeOptionsBottomSheet.kt | 2 +- .../main/sheets/InstallProUpsellBottomSheet.kt | 2 +- .../base/main/sheets/WhatsNewBottomSheet.kt | 2 +- .../base/main/specs/MainActivityBottomBarSpec.kt | 7 +++---- .../base/note/actions/NoteOptionsBottomSheet.kt | 5 ----- .../base/note/actions/TextToSpeechBottomSheet.kt | 4 ++-- .../creation/activity/NoteIntentRouterActivity.kt | 2 +- .../activity/ShareToScarletRouterActivity.kt | 4 ---- .../note/creation/activity/ViewNoteActivity.kt | 4 ++-- .../creation/sheet/MarkdownHelpBottomSheet.kt | 2 +- .../scarlet/base/note/folder/FolderExtensions.kt | 1 - .../note/folder/SelectorFolderRecyclerItem.kt | 4 ++-- .../folder/sheet/CreateOrEditFolderBottomSheet.kt | 8 +++----- .../folder/sheet/FolderChooserBottomSheetBase.kt | 2 +- .../note/formats/recycler/FormatViewHolderBase.kt | 4 ++-- .../base/note/recycler/NoteRecyclerHolder.kt | 6 ++++-- .../base/note/recycler/NoteRecyclerItem.kt | 9 ++++----- .../note/reminders/sheet/ReminderBottomSheet.kt | 7 +++---- .../activity/SelectableNotesActivityBase.kt | 2 +- .../note/tag/sheet/CreateOrEditTagBottomSheet.kt | 6 +++--- .../base/note/tag/sheet/TagChooserBottomSheet.kt | 2 -- .../security/activity/AppLockActivitySpecs.kt | 10 ++++------ .../base/security/sheets/PincodeBottomSheet.kt | 15 ++++++++------- .../scarlet/base/service/FloatingNoteService.kt | 4 ++-- .../settings/sheet/ThemeColorPickerBottomSheet.kt | 5 ++--- .../maubis/scarlet/base/support/ShortcutUtils.kt | 2 -- .../base/support/database/MigrationUtils.kt | 2 +- .../base/support/sheets/GridBottomSheetBase.kt | 2 +- .../base/support/sheets/LithoBottomSheet.kt | 6 +++--- .../sheets/LithoChooseOptionBottomSheet.kt | 2 +- .../base/support/sheets/LithoOptionBottomSheet.kt | 4 ++-- .../base/support/specs/BottomSheetBarSpec.kt | 4 ++-- .../base/support/specs/CounterChooserSpec.kt | 2 +- .../base/support/specs/GridSectionViewSpec.kt | 6 +++--- .../scarlet/base/support/specs/SpecUtils.kt | 4 ++-- .../scarlet/base/support/ui/CircleDrawable.kt | 2 +- .../base/support/ui/LithoCircleDrawable.kt | 2 +- .../scarlet/base/support/ui/ThemeManager.kt | 1 - .../scarlet/base/support/ui/ThemedActivity.kt | 15 +++++---------- .../base/support/ui/ThemedBottomSheetFragment.kt | 10 +++++----- .../scarlet/base/widget/AllNotesWidgetService.kt | 1 - .../main/java/com/bijoysingh/quicknote/Scarlet.kt | 3 +-- .../quicknote/drive/GDriveActivitySpecs.kt | 15 +++++++-------- .../quicknote/drive/GDriveLogoutActivitySpecs.kt | 8 ++++---- .../quicknote/drive/GDrivePendingBottomSheet.kt | 5 ++--- .../quicknote/drive/GDriveRemoteFolderBase.kt | 5 ++++- .../firebase/activity/FirebaseActivitySpecs.kt | 12 ++++++------ .../firebase/activity/FirebaseRemovalActivity.kt | 4 ---- .../activity/FirebaseRemovalActivitySpecs.kt | 15 +++++++-------- .../quicknote/scarlet/ScarletAuthenticator.kt | 5 ++++- 70 files changed, 155 insertions(+), 194 deletions(-) diff --git a/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt b/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt index 697f1147..e3f22afb 100644 --- a/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt +++ b/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt @@ -9,7 +9,6 @@ class MaterialNotes : ApplicationBase() { override fun onCreate() { super.onCreate() ApplicationBase.instance = MaterialNoteConfig(this) - ApplicationBase.instance.themeController().setup(this) ExternalFolderSync.setup(this) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 230ceb6c..d653f3d5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -98,7 +98,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { if (sAutomaticTheme) { setThemeFromSystem(this) } - instance.themeController().notifyChange(this) + ApplicationBase.sAppTheme.notifyChange(this) if (shouldShowWhatsNewSheet()) { openSheet(this, WhatsNewBottomSheet()) @@ -515,7 +515,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { override fun notifyThemeChange() { setSystemTheme() - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme containerLayoutMain.setBackgroundColor(getThemeColor()) val toolbarIconColor = theme.get(ThemeColorType.TOOLBAR_ICON) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt index 4992b874..8c4cea77 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -47,7 +47,7 @@ fun MainActivity.performAction(action: MainActivityActions) { this.onThemeChange = { theme -> if (sAppTheme != theme.name) { sAppTheme = theme.name - ApplicationBase.instance.themeController().notifyChange(activity) + ApplicationBase.sAppTheme.notifyChange(activity) activity.startActivity(MainActivityActions.COLOR_PICKER.intent(activity)) activity.finish() } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index 5c9de1dd..f6159397 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -6,11 +6,11 @@ import com.facebook.soloader.SoLoader import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator +import com.maubis.scarlet.base.support.ui.ThemeManager import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.ImageCache import com.maubis.scarlet.base.support.utils.maybeThrow import com.maubis.scarlet.base.support.utils.sDateFormat -import java.lang.Exception -import java.lang.ref.WeakReference abstract class ApplicationBase : Application() { override fun onCreate() { @@ -23,11 +23,22 @@ abstract class ApplicationBase : Application() { maybeThrow(exception) } noteImagesFolder = NoteImage(this) + + // Setup Image Cache + sImageCache = ImageCache(this) + + // Setup Application Theme + sAppTheme = ThemeManager() + sAppTheme.setup(this) } companion object { lateinit var noteImagesFolder: NoteImage lateinit var instance: CoreConfig + + lateinit var sImageCache: ImageCache + lateinit var sAppTheme: ThemeManager + var folderSync: FolderRemoteDatabase? = null } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 3d6c465d..07e5ee90 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -22,9 +22,7 @@ import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.support.ui.IThemeManager import com.maubis.scarlet.base.support.utils.Flavor -import com.maubis.scarlet.base.support.utils.ImageCache abstract class CoreConfig(context: Context) { @@ -54,8 +52,6 @@ abstract class CoreConfig(context: Context) { abstract fun folderActions(folder: Folder): IFolderActor - abstract fun themeController(): IThemeManager - abstract fun remoteConfigFetcher(): IRemoteConfigFetcher abstract fun remoteDatabaseState(): IRemoteDatabaseState @@ -66,8 +62,6 @@ abstract class CoreConfig(context: Context) { abstract fun store(): Store - abstract fun imageCache(): ImageCache - companion object { val notesDb get() = instance.notesDatabase() val tagsDb get() = instance.tagsDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 5a5ca6a5..841e7eb0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -22,10 +22,7 @@ import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.support.ui.IThemeManager -import com.maubis.scarlet.base.support.ui.ThemeManager import com.maubis.scarlet.base.support.utils.Flavor -import com.maubis.scarlet.base.support.utils.ImageCache const val USER_PREFERENCES_STORE_NAME = "USER_PREFERENCES"; const val USER_PREFERENCES_VERSION = 1; @@ -37,8 +34,6 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { val tagsProvider = TagsProvider() val foldersProvider = FoldersProvider() val store = VersionedStore.get(context, USER_PREFERENCES_STORE_NAME, USER_PREFERENCES_VERSION) - val appTheme = ThemeManager() - val imageCache = ImageCache(context) override fun database(): AppDatabase = db @@ -56,8 +51,6 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun folderActions(folder: Folder): IFolderActor = MaterialFolderActor(folder) - override fun themeController(): IThemeManager = appTheme - override fun remoteConfigFetcher(): IRemoteConfigFetcher = NullRemoteConfigFetcher() override fun remoteDatabaseState(): IRemoteDatabaseState { @@ -72,6 +65,4 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun appFlavor(): Flavor = Flavor.NONE override fun store(): Store = store - - override fun imageCache(): ImageCache = imageCache } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index 644f6f64..be789865 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -3,7 +3,6 @@ package com.maubis.scarlet.base.core.note import android.app.NotificationManager import android.content.Context import android.os.AsyncTask -import android.os.Build import android.support.v7.app.AppCompatActivity import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils @@ -109,7 +108,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { notifyAllChanged(context) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? notificationManager?.cancel(note.uid) - ApplicationBase.instance.imageCache().deleteNote(note.uuid) + ApplicationBase.sImageCache.deleteNote(note.uuid) } protected fun onNoteUpdated(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt index cbc42292..469453bd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt @@ -6,7 +6,6 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.support.utils.throwOrReturn -import java.util.* fun Note.isUnsaved(): Boolean { return this.uid === null || this.uid == 0 diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt index 7baf08cc..8e2426a8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt @@ -67,7 +67,7 @@ class NoteImage(context: Context) { return@launch } - val bitmap = ApplicationBase.instance.imageCache().loadFromCache(file) + val bitmap = ApplicationBase.sImageCache.loadFromCache(file) if (bitmap === null) { deleteIfExist(file) GlobalScope.launch(Dispatchers.Main) { @@ -88,8 +88,8 @@ class NoteImage(context: Context) { image: ImageView, callback: ImageLoadCallback? = null) { GlobalScope.launch { - val thumbnailFile = ApplicationBase.instance.imageCache().thumbnailFile(noteUUID, imageUuid) - val persistentFile = ApplicationBase.instance.imageCache().persistentFile(noteUUID, imageUuid) + val thumbnailFile = ApplicationBase.sImageCache.thumbnailFile(noteUUID, imageUuid) + val persistentFile = ApplicationBase.sImageCache.persistentFile(noteUUID, imageUuid) if (!persistentFile.exists()) { GlobalScope.launch(Dispatchers.Main) { @@ -100,7 +100,7 @@ class NoteImage(context: Context) { } if (thumbnailFile.exists()) { - val bitmap = ApplicationBase.instance.imageCache().loadFromCache(thumbnailFile) + val bitmap = ApplicationBase.sImageCache.loadFromCache(thumbnailFile) if (bitmap === null) { deleteIfExist(thumbnailFile) GlobalScope.launch(Dispatchers.Main) { @@ -117,7 +117,7 @@ class NoteImage(context: Context) { return@launch } - val persistentBitmap = ApplicationBase.instance.imageCache().loadFromCache(persistentFile) + val persistentBitmap = ApplicationBase.sImageCache.loadFromCache(persistentFile) if (persistentBitmap === null) { deleteIfExist(persistentFile) GlobalScope.launch(Dispatchers.Main) { @@ -127,7 +127,7 @@ class NoteImage(context: Context) { return@launch } - val compressedBitmap = ApplicationBase.instance.imageCache().saveThumbnail(thumbnailFile, persistentBitmap) + val compressedBitmap = ApplicationBase.sImageCache.saveThumbnail(thumbnailFile, persistentBitmap) GlobalScope.launch(Dispatchers.Main) { image.visibility = View.VISIBLE image.setImageBitmap(compressedBitmap) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt index 6195019c..58015747 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt @@ -16,7 +16,6 @@ import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.bind import java.io.File import java.io.FileReader @@ -96,13 +95,13 @@ class ImportNoteActivity : SecuredActivity() { } override fun notifyThemeChange() { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme background.setBackgroundColor(theme.get(ThemeColorType.BACKGROUND)) backButton.setColorFilter(theme.get(ThemeColorType.TOOLBAR_ICON)) pageTitle.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setBackgroundResource( - if (ApplicationBase.instance.themeController().isNightTheme()) R.drawable.light_circular_border_bg + if (ApplicationBase.sAppTheme.isNightTheme()) R.drawable.light_circular_border_bg else R.drawable.dark_circular_border_bg) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt index 03080e51..59c4715e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt @@ -13,7 +13,6 @@ import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.sDateFormat import java.io.File @@ -26,7 +25,7 @@ class FileImportViewHolder(context: Context, root: View) private val fileSize: TextView = findViewById(R.id.file_size) init { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme fileName.setTextColor(theme.get(ThemeColorType.SECONDARY_TEXT)) filePath.setTextColor(theme.get(ThemeColorType.HINT_TEXT)) fileDate.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) @@ -44,7 +43,7 @@ class FileImportViewHolder(context: Context, root: View) (context as ImportNoteActivity).select(item) } root.setBackgroundColor( - if (item.selected) ApplicationBase.instance.themeController().get( + if (item.selected) ApplicationBase.sAppTheme.get( context, R.color.material_grey_100, R.color.dark_hint_text) else Color.TRANSPARENT) } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt index ecd506d3..31fbbdcf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt @@ -52,7 +52,7 @@ class ExportNotesBottomSheet : LithoBottomSheet() { .text(filenameRender) .typeface(Typeface.MONOSPACE) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) getOptions(componentContext).forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index 39067d79..3023d43c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -41,21 +41,21 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.import_export_layout_folder_sync_description) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .typeface(FONT_MONSERRAT) .textRes(R.string.import_export_layout_folder_sync_folder) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .text(sFolderSyncPath) .typeface(Typeface.MONOSPACE) .paddingDip(YogaEdge.HORIZONTAL, 20f) .paddingDip(YogaEdge.VERTICAL, 8f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) getOptions(componentContext).forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt index 8c30f5fb..fea886da 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt @@ -29,7 +29,7 @@ class PermissionBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.permission_layout_give_permission_details) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.permission_layout_give_permission_ok) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index 42194a67..81d369ca 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -3,7 +3,6 @@ package com.maubis.scarlet.base.export.support import android.Manifest import android.content.Context import android.content.pm.PackageManager -import android.os.Build import android.support.v4.content.ContextCompat import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.R diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 5a9830f0..1203c7e3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -142,15 +142,15 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { val containerLayout = findViewById(R.id.container_layout); containerLayout.setBackgroundColor(getThemeColor()); - val toolbarIconColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON); + val toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON); backButton.setColorFilter(toolbarIconColor) - val textColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val textColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) filename.setTextColor(textColor) title.setTextColor(textColor) content.setTextColor(textColor) - val actionColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON) + val actionColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) actionDone.setImageTint(actionColor) actionDone.setTextColor(actionColor) } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt index 7a1020c9..432463a7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt @@ -2,7 +2,6 @@ package com.maubis.scarlet.base.main.activity import android.app.Activity import android.app.Application -import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index 67803cd3..6059de0c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -37,10 +37,10 @@ class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerView SettingsOptionsBottomSheet.openSheet((context as MainActivity)) } - val titleColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val titleColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) toolbarTitle.setTextColor(titleColor) - val toolbarIconColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) toolbarIconSearch.setColorFilter(toolbarIconColor) toolbarIconSettings.setColorFilter(toolbarIconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt index 37cdcddd..d6d5ae74 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt @@ -60,7 +60,7 @@ class AlertBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(config.description) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(config.positiveText) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt index 93f36575..6521690b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt @@ -1,6 +1,8 @@ package com.maubis.scarlet.base.main.sheets import android.app.Dialog +import android.content.Intent +import android.net.Uri import android.util.Log import com.facebook.litho.Column import com.facebook.litho.Component @@ -14,8 +16,6 @@ import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.ui.ThemeColorType -import android.content.Intent -import android.net.Uri class ExceptionBottomSheet : LithoBottomSheet() { var exception: Exception = RuntimeException() @@ -32,7 +32,7 @@ class ExceptionBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_small) .text(Markdown.render("```\n${Log.getStackTraceString(exception)}\n```", true)) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.exception_sheet_crash_app) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt index 589af913..50427896 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt @@ -37,7 +37,7 @@ class LithoTagOptionsItem( object TagItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoTagOptionsItem): Component { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val selectedColor = when (theme.isNightTheme()) { true -> context.getColor(R.color.material_blue_400) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index 8a68757d..310f0746 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -41,7 +41,7 @@ class InstallProUpsellBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.why_install_pro) .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(GridSectionView.create(componentContext) .maxLines(3) .numColumns(2) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 24c91fe2..10d942ce 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -45,7 +45,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.whats_new_sheet_subtitle) .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(GridSectionView.create(componentContext) .maxLines(3) .numColumns(2) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index c568cdbe..93aa2f6c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -5,7 +5,6 @@ import android.content.pm.ShortcutInfo import android.graphics.Color import android.graphics.drawable.Icon import android.net.Uri -import android.os.Build import android.text.Layout import com.facebook.litho.* import com.facebook.litho.annotations.LayoutSpec @@ -19,7 +18,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT_MEDIUM import com.maubis.scarlet.base.core.folder.FolderBuilder @@ -205,8 +204,8 @@ object MainActivitySyncingNowSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop isSyncHappening: Boolean): Component { val colorConfig = ToolbarColorConfig( - toolbarBackgroundColor = instance.themeController().get(ThemeColorType.TOOLBAR_BACKGROUND), - toolbarIconColor = instance.themeController().get(ThemeColorType.TOOLBAR_ICON) + toolbarBackgroundColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), + toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) ) val syncText = when (isSyncHappening) { true -> R.string.home_syncing_top_layout diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 4f390832..18ade7fb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -1,13 +1,10 @@ package com.maubis.scarlet.base.note.actions import android.app.Dialog -import android.app.PendingIntent import android.content.Intent import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager import android.graphics.drawable.Icon import android.net.Uri -import android.os.Build import android.support.v4.content.ContextCompat import android.view.View import android.widget.GridLayout @@ -48,8 +45,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch -import java.util.* -import kotlin.collections.ArrayList class NoteOptionsBottomSheet() : GridBottomSheetBase() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index 3eff851a..0da9f9fa 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -34,10 +34,10 @@ class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { val nonNullNote = note!! val title = dialog.findViewById(R.id.options_title) - title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) val speakPlayPause = dialog.findViewById(R.id.speak_play_pause) - speakPlayPause.setColorFilter(ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON)) + speakPlayPause.setColorFilter(ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) speakPlayPause.setOnClickListener { val tts = textToSpeech if (tts === null) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt index c4ac4049..651d113c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/NoteIntentRouterActivity.kt @@ -1,8 +1,8 @@ package com.maubis.scarlet.base.note.creation.activity import android.net.Uri -import android.support.v7.app.AppCompatActivity import android.os.Bundle +import android.support.v7.app.AppCompatActivity import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance class NoteIntentRouterActivity : AppCompatActivity() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt index 08f6791f..4819df8b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -1,10 +1,8 @@ package com.maubis.scarlet.base.note.creation.activity import android.content.Intent -import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.Parcelable import android.support.v7.app.AppCompatActivity @@ -23,8 +21,6 @@ import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.support.BitmapHelper import com.maubis.scarlet.base.support.utils.OsVersionUtils import java.io.File -import java.io.FileOutputStream -import java.util.* class ShareToScarletRouterActivity : AppCompatActivity() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 2e14815b..31acf9db 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -152,7 +152,7 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit val bundle = Bundle() bundle.putBoolean(KEY_EDITABLE, editModeValue) bundle.putBoolean(KEY_MARKDOWN_ENABLED, ApplicationBase.instance.store().get(KEY_MARKDOWN_ENABLED, true)) - bundle.putBoolean(KEY_NIGHT_THEME, ApplicationBase.instance.themeController().isNightTheme()) + bundle.putBoolean(KEY_NIGHT_THEME, ApplicationBase.sAppTheme.isNightTheme()) bundle.putInt(STORE_KEY_TEXT_SIZE, sEditorTextSize) bundle.putInt(KEY_NOTE_COLOR, currentNote?.color ?: sNoteDefaultColor) bundle.putString(INTENT_KEY_NOTE_ID, currentNote?.uuid ?: generateUUID()) @@ -257,7 +257,7 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit return } - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme when { !useNoteColorAsBackground -> { colorConfig.backgroundColor = theme.get(ThemeColorType.BACKGROUND) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt index f0605b94..b561cf94 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt @@ -28,7 +28,7 @@ class MarkdownHelpBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_normal) .marginDip(YogaEdge.HORIZONTAL, 20f) .paddingDip(YogaEdge.VERTICAL, 4f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) } return column.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt index a0142ad9..729684f7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderExtensions.kt @@ -3,7 +3,6 @@ package com.maubis.scarlet.base.note.folder import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.database.room.folder.Folder -import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt index 17b688a8..f58e7cb0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt @@ -3,7 +3,7 @@ package com.maubis.scarlet.base.note.folder import android.content.Context import android.support.v4.content.ContextCompat import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ColorUtil @@ -14,7 +14,7 @@ class SelectorFolderRecyclerItem(context: Context, val folder: Folder) : Recycle val isLightShaded = ColorUtil.isLightColored(folder.color) val title = folder.title - val titleColor = instance.themeController().get(ThemeColorType.TERTIARY_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.TERTIARY_TEXT) val folderColor = folder.color val iconColor = when (isLightShaded) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt index 320ddad5..45a54e4e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt @@ -11,12 +11,10 @@ import android.widget.TextView import com.google.android.flexbox.FlexboxLayout import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.folder.isUnsaved import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.note.folder.delete import com.maubis.scarlet.base.note.folder.save -import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.settings.view.ColorView import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -53,9 +51,9 @@ class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { val colorFlexbox = dialog.findViewById(R.id.color_flexbox) val colorCard = dialog.findViewById(R.id.core_color_card) - title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterFolder.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterFolder.setHintTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)) + title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterFolder.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterFolder.setHintTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.HINT_TEXT)) title.setText(if (folder.isUnsaved()) R.string.folder_sheet_add_note else R.string.folder_sheet_edit_note) action.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt index 603c7ca5..647086a2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt @@ -34,7 +34,7 @@ data class FolderOptionsItem( object FolderItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: FolderOptionsItem): Component { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val selectedColor = when (theme.isNightTheme()) { true -> context.getColor(R.color.material_blue_400) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index f2389aa1..1bbebbb7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -46,7 +46,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView val tertiaryTextColor: Int val iconColor: Int val hintTextColor: Int - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val isLightBackground = ColorUtil.isLightColored(noteColor) val linkColor: Int when { @@ -90,7 +90,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView } }(), backgroundColor = when (data.formatType) { - FormatType.CODE, FormatType.IMAGE -> ApplicationBase.instance.themeController().get(context, R.color.code_light, R.color.code_dark) + FormatType.CODE, FormatType.IMAGE -> ApplicationBase.sAppTheme.get(context, R.color.code_light, R.color.code_dark) else -> ContextCompat.getColor(context, R.color.transparent) }, secondaryTextColor = secondaryTextColor, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt index 2d44806f..f3d59282 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt @@ -5,9 +5,11 @@ import android.os.Bundle import android.view.View import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.* - import com.maubis.scarlet.base.note.actions.NoteOptionsBottomSheet +import com.maubis.scarlet.base.note.copy +import com.maubis.scarlet.base.note.edit +import com.maubis.scarlet.base.note.share +import com.maubis.scarlet.base.note.view import com.maubis.scarlet.base.security.sheets.openUnlockSheet import com.maubis.scarlet.base.support.ui.ThemedActivity diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt index ca56e9ea..2303b67c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerItem.kt @@ -7,12 +7,11 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.core.note.getNoteState import com.maubis.scarlet.base.core.note.getReminderV2 import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.* -import com.maubis.scarlet.base.note.creation.sheet.sEditorMarkdownEnabled -import com.maubis.scarlet.base.security.controller.PinLockController -import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.sMarkdownEnabledHome +import com.maubis.scarlet.base.note.getDisplayTime +import com.maubis.scarlet.base.note.getImageFile +import com.maubis.scarlet.base.note.getLockedAwareTextForHomeList +import com.maubis.scarlet.base.note.getTagString import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount -import com.maubis.scarlet.base.settings.sheet.sSecurityAppLockEnabled import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ColorUtil diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt index 02c81f65..0b75a4a0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt @@ -21,7 +21,6 @@ import com.maubis.scarlet.base.support.sheets.LithoChooseOptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment -import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* @@ -210,9 +209,9 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { val reminderTime = dialog.findViewById(R.id.reminder_time) val reminderRepeat = dialog.findViewById(R.id.reminder_repeat) - val iconColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON) - val textColor = ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT) - val titleColor = ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER) + val iconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + val textColor = ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT) + val titleColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER) reminderDate.setTitleColor(titleColor) reminderDate.setSubtitleColor(textColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index 0ad0013f..58e2ca32 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -114,7 +114,7 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct val containerLayout = findViewById(R.id.container_layout) containerLayout.setBackgroundColor(getThemeColor()) - val toolbarIconColor = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON); + val toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON); findViewById(R.id.back_button).setColorFilter(toolbarIconColor) findViewById(R.id.toolbar_title).setTextColor(toolbarIconColor) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt index eba2a680..56e941b1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt @@ -43,9 +43,9 @@ class CreateOrEditTagBottomSheet : ThemedBottomSheetFragment() { val enterTag = dialog.findViewById(R.id.enter_tag) val removeBtn = dialog.findViewById(R.id.action_remove_button) - title.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterTag.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) - enterTag.setHintTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.HINT_TEXT)) + title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterTag.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterTag.setHintTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.HINT_TEXT)) title.setText(if (tag.isUnsaved()) R.string.tag_sheet_create_title else R.string.tag_sheet_edit_title) action.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt index 4535a1d4..7d16c423 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt @@ -6,9 +6,7 @@ import android.support.v7.app.AppCompatActivity import com.facebook.litho.Column import com.facebook.litho.Component import com.facebook.litho.ComponentContext -import com.facebook.litho.widget.EmptyComponent import com.facebook.yoga.YogaEdge -import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.note.getTagUUIDs diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt index 6273c03a..46021481 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -1,21 +1,19 @@ package com.maubis.scarlet.base.security.activity -import android.graphics.Color import android.text.InputType import android.text.Layout -import android.view.KeyEvent import android.view.inputmethod.EditorInfo import com.facebook.litho.* import com.facebook.litho.annotations.* -import com.facebook.litho.widget.* -import com.facebook.litho.widget.Spinner.onClick +import com.facebook.litho.widget.EditText +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.TextChangedEvent import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.security.sheets.PincodeSheetData -import com.maubis.scarlet.base.security.sheets.PincodeSheetViewSpec import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.utils.getEditorActionListener diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index d756ac26..f8b3efde 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -3,11 +3,13 @@ package com.maubis.scarlet.base.security.sheets import android.app.Dialog import android.text.InputType import android.text.Layout -import android.view.KeyEvent import android.view.inputmethod.EditorInfo import com.facebook.litho.* import com.facebook.litho.annotations.* -import com.facebook.litho.widget.* +import com.facebook.litho.widget.EditText +import com.facebook.litho.widget.Image +import com.facebook.litho.widget.Text +import com.facebook.litho.widget.TextChangedEvent import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.github.ajalt.reprint.core.AuthenticationFailureReason @@ -16,7 +18,6 @@ import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.security.controller.PinLockController import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled @@ -60,7 +61,7 @@ object PincodeSheetViewSpec { @Prop data: PincodeSheetData, @Prop dismiss: () -> Unit): Component { val editBackground = when { - ApplicationBase.instance.themeController().isNightTheme() -> R.drawable.light_secondary_rounded_bg + ApplicationBase.sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg else -> R.drawable.secondary_rounded_bg } @@ -75,7 +76,7 @@ object PincodeSheetViewSpec { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.app_lock_details) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(EditText.create(context) .backgroundRes(editBackground) .textSizeRes(R.dimen.font_size_xlarge) @@ -86,7 +87,7 @@ object PincodeSheetViewSpec { .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) .marginDip(YogaEdge.VERTICAL, 8f) @@ -113,7 +114,7 @@ object PincodeSheetViewSpec { when { data.isRemoveButtonEnabled -> Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(instance.themeController().get(ThemeColorType.HINT_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.HINT_TEXT)) .textRes(R.string.security_sheet_button_remove) .textAlignment(Layout.Alignment.ALIGN_CENTER) .paddingDip(YogaEdge.VERTICAL, 12f) diff --git a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt index b091a37c..95deb19b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt @@ -36,7 +36,7 @@ class FloatingNoteService : FloatingBubbleService() { private lateinit var panel: View override fun getConfig(): FloatingBubbleConfig { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme return FloatingBubbleConfig.Builder() .bubbleIcon(ContextCompat.getDrawable(context, R.drawable.app_icon)) .removeBubbleIcon(ContextCompat.getDrawable( @@ -69,7 +69,7 @@ class FloatingNoteService : FloatingBubbleService() { stopSelf() } - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val rootView = getInflater().inflate(R.layout.layout_add_note_overlay, null) description = rootView.findViewById(R.id.description) as TextView diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index c1be819e..14f00fb0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -2,7 +2,6 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog import android.graphics.Color -import android.os.Build import android.support.v7.app.AppCompatActivity import com.facebook.litho.* import com.facebook.litho.annotations.LayoutSpec @@ -12,7 +11,7 @@ import com.facebook.litho.annotations.Prop import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.support.sheets.* import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -105,7 +104,7 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { sAutomaticTheme = !sAutomaticTheme if (sAutomaticTheme) { setThemeFromSystem(context) - onThemeChange(instance.themeController().get()) + onThemeChange(ApplicationBase.sAppTheme.get()) } reset(context, dialog) }) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt index bf3ceb5a..9346d4dc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt @@ -4,9 +4,7 @@ import android.app.PendingIntent import android.content.Context import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager -import android.os.Build import com.maubis.scarlet.base.support.utils.OsVersionUtils -import java.util.* fun addShortcut(context: Context, shortcut: ShortcutInfo) { if (!OsVersionUtils.canAddLauncherShortcuts()) { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index d8df3f1f..40cccca1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -32,7 +32,7 @@ class Migrator(val context: Context) { runTask(KEY_MIGRATE_THEME) { val isNightMode = ApplicationBase.instance.store().get(KEY_NIGHT_THEME, true) sAppTheme = if (isNightMode) Theme.DARK.name else Theme.LIGHT.name - ApplicationBase.instance.themeController().notifyChange(context) + ApplicationBase.sAppTheme.notifyChange(context) } runTask(key = KEY_MIGRATE_REMINDERS) { val notes = notesDb.getAll() diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt index 2a5ecfd2..d46d6942 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt @@ -36,7 +36,7 @@ abstract class GridBottomSheetBase : ThemedBottomSheetFragment() { fun setOptionTitle(dialog: Dialog, title: Int) { GlobalScope.launch(Dispatchers.Main) { val titleView = dialog.findViewById(R.id.options_title) - titleView.setTextColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + titleView.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) titleView.setText(title) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 40ac7016..619124bb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -36,7 +36,7 @@ fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { .marginDip(YogaEdge.TOP, 18f) .marginDip(YogaEdge.BOTTOM, 8f) .textStyle(Typeface.BOLD) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) } fun getLithoBottomSheetButton(context: ComponentContext): Text.Builder { @@ -82,7 +82,7 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { } fun getFullComponent(componentContext: ComponentContext, dialog: Dialog, childComponent: Component) { - val topHandle = when (ApplicationBase.instance.themeController().isNightTheme()) { + val topHandle = when (ApplicationBase.sAppTheme.isNightTheme()) { true -> R.drawable.bottom_sheet_top_handle_dark false -> R.drawable.bottom_sheet_top_handle_light } @@ -118,7 +118,7 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { abstract fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component - open fun backgroundColor(componentContext: ComponentContext) = ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND) + open fun backgroundColor(componentContext: ComponentContext) = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) open fun topMargin() = 16f diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt index 350577b1..1d328d2e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt @@ -27,7 +27,7 @@ object ChooseOptionItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoChooseOptionsItem): Component { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val selectedColor = context.getColor(R.color.colorAccent) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt index ec358bf6..c1bcab46 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt @@ -34,7 +34,7 @@ object OptionItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoOptionsItem): Component { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val subtitleColor = theme.get(ThemeColorType.HINT_TEXT) val selectedColor = theme.get(ThemeColorType.ACCENT_TEXT) @@ -123,7 +123,7 @@ object OptionLabelItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoLabelOptionsItem): Component { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val row = Column.create(context) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt index 9022fe2b..3d0d578c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt @@ -36,7 +36,7 @@ object BottomSheetBarSpec { .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .clickHandler(BottomSheetBar.onSecondaryClickEvent(context))) } row.child(EmptySpec.create(context).flexGrow(1f)) @@ -48,7 +48,7 @@ object BottomSheetBarSpec { .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .clickHandler(BottomSheetBar.onTertiaryClickEvent(context))) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt index f7a87a1b..0143415a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt @@ -34,7 +34,7 @@ object CounterChooserSpec { .typeface(CoreConfig.FONT_MONSERRAT) .textSizeRes(R.dimen.font_size_xxxlarge) .paddingDip(YogaEdge.HORIZONTAL, 12f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(bottomBarRoundIcon(context, ToolbarColorConfig()) .iconRes(R.drawable.icon_more_counter) .onClick { onValueChange(Math.min(value + 1, maxValue)) }) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt index d36a5970..29f876f8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -10,7 +10,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -86,7 +86,7 @@ object GridSectionViewSpec { @Prop(optional = true) maxLines: Int?, @Prop(optional = true) showSeparator: Boolean?): Component { val column = Column.create(context) - val primaryColor = instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val primaryColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) if (section.title != 0) { column.child( @@ -138,7 +138,7 @@ object GridSectionViewSpec { if (showSeparator == true) { column.child(SolidColor.create(context) - .color(instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .color(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .heightDip(1.5f) .widthDip(196f) .alignSelf(YogaAlign.CENTER) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt index 3ab7a470..4feca05f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt @@ -41,8 +41,8 @@ fun separatorSpec(context: ComponentContext): Component.Builder<*> { data class ToolbarColorConfig( - var toolbarBackgroundColor: Int = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_BACKGROUND), - var toolbarIconColor: Int = ApplicationBase.instance.themeController().get(ThemeColorType.TOOLBAR_ICON)) + var toolbarBackgroundColor: Int = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), + var toolbarIconColor: Int = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) fun bottomBarRoundIcon(context: ComponentContext, colorConfig: ToolbarColorConfig): RoundIcon.Builder { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt index 83f58bfe..7d8fa248 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt @@ -13,7 +13,7 @@ class CircleDrawable(color: Int, showBorder: Boolean = true) : Drawable() { this.paint = Paint(Paint.ANTI_ALIAS_FLAG) this.paint.color = color - val isNightTheme = ApplicationBase.instance.themeController().isNightTheme() + val isNightTheme = ApplicationBase.sAppTheme.isNightTheme() this.borderPaint = Paint(Paint.ANTI_ALIAS_FLAG) this.borderPaint.color = when { !showBorder -> Color.TRANSPARENT diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt index 6b9bbb6e..cabc317f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt @@ -14,7 +14,7 @@ class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean this.mPaint.color = color this.mPaint.alpha = alpha - val isNightTheme = ApplicationBase.instance.themeController().isNightTheme() + val isNightTheme = ApplicationBase.sAppTheme.isNightTheme() this.mBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG) this.mBorderPaint.color = when { !showBorder -> Color.TRANSPARENT diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index c18b49fd..0fc1ec61 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -3,7 +3,6 @@ package com.maubis.scarlet.base.support.ui import android.content.Context import android.content.res.Configuration import android.graphics.Color -import android.os.Build import android.support.v4.content.ContextCompat import com.github.bijoysingh.starter.util.DimensionManager import com.maubis.markdown.MarkdownConfig diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index 219ae310..bc43e362 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -2,16 +2,11 @@ package com.maubis.scarlet.base.support.ui import android.content.Context import android.content.res.Configuration -import android.os.Build import android.os.Bundle -import android.os.PersistableBundle import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager -import com.maubis.scarlet.base.BuildConfig -import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.settings.sheet.sInternalEnableFullScreen import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.maybeThrow @@ -26,7 +21,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - instance.themeController().register(this) + ApplicationBase.sAppTheme.register(this) } fun setSystemTheme(color: Int = getStatusBarColor()) { @@ -45,7 +40,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { return } setThemeFromSystem(this) - instance.themeController().notifyChange(this) + ApplicationBase.sAppTheme.notifyChange(this) } fun fullScreenView() { @@ -75,7 +70,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { if (OsVersionUtils.canSetStatusBarTheme()) { val view = window.decorView var flags = view.systemUiVisibility - flags = when (ApplicationBase.instance.themeController().isNightTheme()) { + flags = when (ApplicationBase.sAppTheme.isNightTheme()) { true -> flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() false -> flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } @@ -83,9 +78,9 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { } } - fun getThemeColor(): Int = ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND) + fun getThemeColor(): Int = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) - fun getStatusBarColor(): Int = ApplicationBase.instance.themeController().get(ThemeColorType.STATUS_BAR) + fun getStatusBarColor(): Int = ApplicationBase.sAppTheme.get(ThemeColorType.STATUS_BAR) fun tryClosingTheKeyboard() { try { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt index d2b4725e..83f87914 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt @@ -45,7 +45,7 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { abstract fun getBackgroundView(): Int fun resetBackground(dialog: Dialog) { - val backgroundColor = ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND) + val backgroundColor = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) val containerLayout = dialog.findViewById(getBackgroundView()) containerLayout.setBackgroundColor(backgroundColor) for (viewId in getBackgroundCardViewIds()) { @@ -56,8 +56,8 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { open fun getOptionsTitleColor(selected: Boolean): Int { val colorResource = when { - ApplicationBase.instance.themeController().isNightTheme() && selected -> R.color.material_blue_300 - ApplicationBase.instance.themeController().isNightTheme() -> R.color.light_secondary_text + ApplicationBase.sAppTheme.isNightTheme() && selected -> R.color.material_blue_300 + ApplicationBase.sAppTheme.isNightTheme() -> R.color.light_secondary_text selected -> R.color.material_blue_700 else -> R.color.dark_secondary_text } @@ -66,8 +66,8 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { open fun getOptionsSubtitleColor(selected: Boolean): Int { val colorResource = when { - ApplicationBase.instance.themeController().isNightTheme() && selected -> R.color.material_blue_200 - ApplicationBase.instance.themeController().isNightTheme() -> R.color.light_tertiary_text + ApplicationBase.sAppTheme.isNightTheme() && selected -> R.color.material_blue_200 + ApplicationBase.sAppTheme.isNightTheme() -> R.color.light_tertiary_text selected -> R.color.material_blue_500 else -> R.color.dark_tertiary_text } diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt index 307dea90..50a39605 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt @@ -10,7 +10,6 @@ import android.widget.RemoteViewsService import com.maubis.scarlet.base.R import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID -import com.maubis.scarlet.base.note.creation.activity.ViewAdvancedNoteActivity import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.widget.sheet.getWidgetNoteText import com.maubis.scarlet.base.widget.sheet.getWidgetNotes diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index c32d8c6b..a5a65519 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -1,7 +1,7 @@ package com.bijoysingh.quicknote -import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase import com.bijoysingh.quicknote.database.RemoteDatabaseStateController +import com.bijoysingh.quicknote.drive.GDriveRemoteDatabase import com.bijoysingh.quicknote.firebase.FirebaseRemoteDatabase import com.bijoysingh.quicknote.scarlet.ScarletConfig import com.github.bijoysingh.starter.prefs.Store @@ -16,7 +16,6 @@ class Scarlet : ApplicationBase() { remoteConfig = Store.get(this, "gdrive_config") instance = ScarletConfig(this) - instance.themeController().setup(this) instance.authenticator().setup(this) instance.remoteConfigFetcher().setup(this) ExternalFolderSync.setup(this) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index ffdc0cde..7f399e86 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -14,7 +14,6 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.LithoCircleDrawable import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -29,7 +28,7 @@ object GDriveRootViewSpec { else -> R.string.google_drive_page_login_button } return Column.create(context) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -39,7 +38,7 @@ object GDriveRootViewSpec { .marginDip(YogaEdge.HORIZONTAL, 16f) .paddingDip(YogaEdge.VERTICAL, 8f) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.google_drive_page_login_firebase_button) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_MONSERRAT) @@ -84,15 +83,15 @@ object GDriveContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_login_title) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_login_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(GDriveIconView.create(context) @@ -119,7 +118,7 @@ object GDriveIconViewSpec { .paddingDip(YogaEdge.HORIZONTAL, 32f) .paddingDip(YogaEdge.VERTICAL, 24f) .child(Image.create(context) - .drawable(icon.color(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) + .drawable(icon.color(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) .paddingDip(YogaEdge.ALL, 12f) .marginDip(YogaEdge.BOTTOM, 12f) @@ -128,7 +127,7 @@ object GDriveIconViewSpec { .text(title) .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT)) .build() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt index 2763620b..1409e441 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt @@ -26,7 +26,7 @@ object GDriveLogoutRootViewSpec { else -> R.string.google_drive_page_logout_button } return Column.create(context) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -65,15 +65,15 @@ object GDriveLogoutContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_logout_title) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_logout_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(GDriveIconView.create(context) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index d6ccbfac..86937cc7 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -24,7 +24,6 @@ import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.sDateFormat import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope @@ -42,7 +41,7 @@ object PendingItemIconSpec { fun onCreate(context: ComponentContext, @Prop(resType = ResType.STRING) label: String, @Prop(resType = ResType.DRAWABLE) icon: Drawable): Component { - val secondaryColor = instance.themeController().get(ThemeColorType.SECONDARY_TEXT) + val secondaryColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) return Row.create(context) .paddingDip(YogaEdge.HORIZONTAL, 8f) .paddingDip(YogaEdge.VERTICAL, 4f) @@ -67,7 +66,7 @@ object PendingItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: PendingItem): Component { - val theme = ApplicationBase.instance.themeController() + val theme = ApplicationBase.sAppTheme val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) val subtitleColor = theme.get(ThemeColorType.TERTIARY_TEXT) val hintColor = theme.get(ThemeColorType.HINT_TEXT) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 71e90058..4dc7914e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -1,6 +1,9 @@ package com.bijoysingh.quicknote.drive -import com.bijoysingh.quicknote.database.* +import com.bijoysingh.quicknote.database.RemoteDataType +import com.bijoysingh.quicknote.database.RemoteDatabaseHelper +import com.bijoysingh.quicknote.database.RemoteFolder +import com.bijoysingh.quicknote.database.RemoteUploadDataDao import com.google.api.services.drive.model.File import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt index 3f8ff3d8..f94d4ce3 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt @@ -27,7 +27,7 @@ object FirebaseRootViewSpec { else -> R.string.firebase_page_login_button } return Column.create(context) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -66,15 +66,15 @@ object FirebaseContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.firebase_page_login_title) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_page_important_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(FirebaseIconView.create(context) @@ -101,7 +101,7 @@ object FirebaseIconViewSpec { .paddingDip(YogaEdge.HORIZONTAL, 32f) .paddingDip(YogaEdge.VERTICAL, 24f) .child(Image.create(context) - .drawable(icon.color(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT))) + .drawable(icon.color(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) .paddingDip(YogaEdge.ALL, 12f) .marginDip(YogaEdge.BOTTOM, 12f) @@ -110,7 +110,7 @@ object FirebaseIconViewSpec { .text(title) .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT)) .build() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt index a49c1d45..c92140de 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt @@ -7,7 +7,6 @@ import android.os.Handler import com.bijoysingh.quicknote.R import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity.Companion.firebaseForgetMe import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity.Companion.forgettingInProcess -import com.bijoysingh.quicknote.scarlet.sFirebaseKilled import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView @@ -16,9 +15,6 @@ import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.api.GoogleApiClient -import com.google.android.gms.common.api.Scope -import com.google.api.services.drive.DriveScopes -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.support.ui.ThemedActivity import java.util.concurrent.atomic.AtomicBoolean diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt index aea13938..44f42cde 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt @@ -1,7 +1,6 @@ package com.bijoysingh.quicknote.firebase.activity import android.text.Layout -import com.bijoysingh.quicknote.drive.GDriveRootView import com.facebook.litho.* import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout @@ -27,7 +26,7 @@ object FirebaseRemovalRootViewSpec { else -> R.string.firebase_removal_page_clear_button } return Column.create(context) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -66,15 +65,15 @@ object FirebaseRemovalContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.firebase_removal_page_login_title) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_removal_page_important_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(Row.create(context) @@ -96,7 +95,7 @@ object FirebaseRemovalContentViewSpec { .paddingDip(YogaEdge.BOTTOM, 10f) .paddingDip(YogaEdge.TOP, 20f) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_whats_next_details) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) @@ -105,7 +104,7 @@ object FirebaseRemovalContentViewSpec { .marginDip(YogaEdge.HORIZONTAL, 16f) .paddingDip(YogaEdge.ALL, 12f) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_remove_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(Text.create(context) @@ -114,7 +113,7 @@ object FirebaseRemovalContentViewSpec { .marginDip(YogaEdge.HORIZONTAL, 16f) .paddingDip(YogaEdge.ALL, 12f) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_next_details) .typeface(CoreConfig.FONT_MONSERRAT)) .build() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt index 91e81a8b..538859ed 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletAuthenticator.kt @@ -5,7 +5,10 @@ import android.content.Intent import com.bijoysingh.quicknote.Scarlet import com.bijoysingh.quicknote.Scarlet.Companion.gDrive import com.bijoysingh.quicknote.database.RemoteDatabaseStateController -import com.bijoysingh.quicknote.drive.* +import com.bijoysingh.quicknote.drive.GDriveAuthenticator +import com.bijoysingh.quicknote.drive.GDriveLoginActivity +import com.bijoysingh.quicknote.drive.GDriveLogoutActivity +import com.bijoysingh.quicknote.drive.GDrivePendingBottomSheet import com.bijoysingh.quicknote.firebase.activity.FirebaseRemovalActivity import com.bijoysingh.quicknote.firebase.activity.ForgetMeActivity import com.bijoysingh.quicknote.firebase.support.FirebaseAuthenticator From 645abf30a402ee481b47d69d44926f839e225c59 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 30 Oct 2019 23:17:50 +0000 Subject: [PATCH 109/134] [Refactor][1.2] Fixing build issue because of missing conversion --- .../java/com/maubis/scarlet/base/MainActivity.kt | 1 - .../maubis/scarlet/base/MainActivityExtensions.kt | 6 +++--- .../export/sheet/ExternalFolderSyncBottomSheet.kt | 7 ++----- .../base/main/specs/MainActivityBottomBarSpec.kt | 1 - .../base/note/actions/TextToSpeechBottomSheet.kt | 5 +---- .../folder/sheet/CreateOrEditFolderBottomSheet.kt | 1 - .../note/formats/recycler/FormatListViewHolder.kt | 4 ++-- .../base/note/reminders/sheet/ReminderBottomSheet.kt | 1 - .../activity/SelectableNotesActivityBase.kt | 1 - .../sheet/SelectedNotesOptionsBottomSheet.kt | 4 ++-- .../scarlet/base/notification/NotificationHandler.kt | 9 ++++----- .../base/security/activity/AppLockActivitySpecs.kt | 12 ++++++------ .../base/security/sheets/NoPincodeBottomSheet.kt | 2 +- .../base/settings/sheet/AboutUsBottomSheet.kt | 10 +++++----- .../base/settings/sheet/ColorPickerBottomSheet.kt | 6 +++--- .../base/settings/sheet/FontSizeBottomSheet.kt | 2 +- .../base/settings/sheet/OpenSourceBottomSheet.kt | 6 +++--- .../settings/sheet/UISettingsOptionsBottomSheet.kt | 3 +-- .../scarlet/base/support/database/MigrationUtils.kt | 6 +++--- .../base/support/ui/BottomSheetTabletDialog.kt | 4 ++-- .../maubis/scarlet/base/support/ui/ThemeManager.kt | 8 ++++---- .../maubis/scarlet/base/support/ui/ThemedActivity.kt | 4 +--- .../maubis/scarlet/base/support/utils/FlavorUtils.kt | 7 +++---- .../scarlet/base/support/utils/OsVersionUtils.kt | 4 ---- .../quicknote/drive/GDrivePendingBottomSheet.kt | 2 +- 25 files changed, 48 insertions(+), 68 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index d653f3d5..85bb97ad 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -445,7 +445,6 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { HomeNavigationState.TRASH -> onTrashClick() HomeNavigationState.LOCKED -> onLockedClick() HomeNavigationState.DEFAULT -> onHomeClick() - else -> onHomeClick() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt index 8c4cea77..281da17d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -5,7 +5,7 @@ import android.content.Intent import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.settings.sheet.ThemeColorPickerBottomSheet import com.maubis.scarlet.base.support.sheets.openSheet -import com.maubis.scarlet.base.support.ui.sAppTheme +import com.maubis.scarlet.base.support.ui.sAppThemeLabel const val INTENT_KEY_ADDITIONAL_ACTION = "additional_action" @@ -45,8 +45,8 @@ fun MainActivity.performAction(action: MainActivityActions) { MainActivityActions.COLOR_PICKER -> { openSheet(this, ThemeColorPickerBottomSheet().apply { this.onThemeChange = { theme -> - if (sAppTheme != theme.name) { - sAppTheme = theme.name + if (sAppThemeLabel != theme.name) { + sAppThemeLabel = theme.name ApplicationBase.sAppTheme.notifyChange(activity) activity.startActivity(MainActivityActions.COLOR_PICKER.intent(activity)) activity.finish() diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index 3023d43c..ef9ed699 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -28,8 +28,6 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity class ExternalFolderSyncBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { - val activity = componentContext.androidContext as ThemedActivity - val component = Column.create(componentContext) .widthPercent(100f) .paddingDip(YogaEdge.VERTICAL, 8f) @@ -58,7 +56,7 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) - getOptions(componentContext).forEach { + getOptions().forEach { if (it.visible) { component.child(OptionItemLayout.create(componentContext) .option(it) @@ -82,8 +80,7 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { return component.build() } - fun getOptions(componentContext: ComponentContext): List { - val activity = componentContext.androidContext as MainActivity + fun getOptions(): List { val options = ArrayList() options.add(LithoOptionsItem( title = R.string.import_export_locked, diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 93aa2f6c..62b95fd7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -162,7 +162,6 @@ object MainActivityDisabledSyncSpec { toolbarBackgroundColor = context.getColor(R.color.material_blue_grey_800), toolbarIconColor = context.getColor(R.color.light_secondary_text) ) - val activity = context.androidContext as MainActivity val row = Row.create(context) .widthPercent(100f) .alignItems(YogaAlign.CENTER) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index 0da9f9fa..8bd467e1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -64,10 +64,7 @@ class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { } private fun speak(note: Note) { - when (OsVersionUtils.requiresTTSUtteranceId()) { - true -> textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null, "NOTE") - false -> textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null) - } + textToSpeech?.speak(note.getTextToSpeechText(), TextToSpeech.QUEUE_FLUSH, null, "NOTE") } override fun getBackgroundView(): Int = R.id.container_layout diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt index 45a54e4e..ffcdc16f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt @@ -49,7 +49,6 @@ class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { val enterFolder = dialog.findViewById(R.id.enter_folder) val removeBtn = dialog.findViewById(R.id.action_remove_button) val colorFlexbox = dialog.findViewById(R.id.color_flexbox) - val colorCard = dialog.findViewById(R.id.core_color_card) title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) enterFolder.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt index 57373704..9f97695f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt @@ -20,10 +20,10 @@ class FormatListViewHolder(context: Context, view: View) : FormatTextViewHolder( init { edit.setOnEditorActionListener(getEditorActionListener( runnable = { - activity.createOrChangeToNextFormat(format!!) + activity.createOrChangeToNextFormat(format) true }, - preConditions = { format === null || !edit.isFocused } + preConditions = {!edit.isFocused } )) edit.imeOptions = EditorInfo.IME_ACTION_DONE edit.setRawInputType(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt index 0b75a4a0..9595f119 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt @@ -197,7 +197,6 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { val reminderTime = dialog.findViewById(R.id.reminder_time) val reminderRepeat = dialog.findViewById(R.id.reminder_repeat) - val date = Date(reminder.timestamp) reminderRepeat.setSubtitle(getReminderIntervalLabel(reminder.interval)) reminderTime.setSubtitle(sDateFormat.readableTime(DateFormatter.Formats.HH_MM_A.format, reminder.timestamp)) reminderDate.setSubtitle(sDateFormat.readableTime(DateFormatter.Formats.DD_MMM_YYYY.format, reminder.timestamp)) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index 58e2ca32..3f283c22 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -73,7 +73,6 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct adapter.addItem(EmptyRecyclerItem()) } - var lastFolder = "" notes.forEach { adapter.addItem(it) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt index f2fd794e..00d6d46f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt @@ -27,13 +27,13 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val options = ArrayList() - options.add(getQuickActions(componentContext, dialog)) + options.add(getQuickActions(componentContext)) options.add(getSecondaryActions(componentContext, dialog)) options.add(getTertiaryActions(componentContext, dialog)) return options } - private fun getQuickActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { + private fun getQuickActions(componentContext: ComponentContext): GridSectionItem { val activity = componentContext.androidContext as SelectNotesActivity val options = ArrayList() diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt index c9680ac5..2c613b75 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt @@ -95,17 +95,16 @@ class NotificationHandler(val context: Context) { contentView.setTextViewText(R.id.description, config.note.getTextForSharing()) contentView.setTextViewText(R.id.timestamp, config.note.getDisplayTime()) - val theme = ApplicationBase.instance.themeController() - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) - val descColor = theme.get(ThemeColorType.TERTIARY_TEXT) + val titleColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val descColor = ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT) contentView.setTextColor(R.id.title, titleColor) contentView.setTextColor(R.id.description, titleColor) contentView.setTextColor(R.id.timestamp, descColor) - val backgroundColor = theme.get(ThemeColorType.BACKGROUND) + val backgroundColor = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) contentView.setInt(R.id.root_layout, "setBackgroundColor", backgroundColor) - val iconColor = theme.get(ThemeColorType.TOOLBAR_ICON) + val iconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) contentView.setInt(R.id.options_button, "setColorFilter", iconColor) contentView.setInt(R.id.copy_button, "setColorFilter", iconColor) contentView.setInt(R.id.share_button, "setColorFilter", iconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt index 46021481..6f47916f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -27,7 +27,7 @@ object AppLockViewSpec { @Prop onTextChange: (String) -> Unit, @Prop onClick: () -> Unit): Component { return Column.create(context) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(AppLockContentView.create(context) .fingerprintEnabled(fingerprintEnabled) .onTextChange(onTextChange) @@ -78,21 +78,21 @@ object AppLockContentViewSpec { else -> R.string.app_lock_details_no_fingerprint } val editBackground = when { - ApplicationBase.instance.themeController().isNightTheme() -> R.drawable.light_secondary_rounded_bg + ApplicationBase.sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg else -> R.drawable.secondary_rounded_bg } return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.instance.themeController().get(ThemeColorType.BACKGROUND)) + .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.app_lock_title) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECONDARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(description) .typeface(CoreConfig.FONT_MONSERRAT)) .child(EmptySpec.create(context).flexGrow(1f)) @@ -106,7 +106,7 @@ object AppLockContentViewSpec { .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.PRIMARY_TEXT)) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) .imeOptions(EditorInfo.IME_ACTION_DONE) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt index b9afba74..fca47348 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt @@ -36,7 +36,7 @@ class NoPincodeBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.no_pincode_sheet_details) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.no_pincode_sheet_set_up) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index c97cac24..932c08e0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -45,29 +45,29 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutUsDetails) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_about_app) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutAppDetails) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_app_version) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(version) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.about_page_rate) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt index 19b399c0..a5e316a0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt @@ -84,9 +84,9 @@ class ColorPickerBottomSheet : LithoBottomSheet() { ColorPickerItem.create(componentContext) .color(color) .isSelected(color == config.selectedColor) - .onColorSelected { color -> - config.selectedColor = color - config.onColorSelected(color) + .onColorSelected { selectedColor -> + config.selectedColor = selectedColor + config.onColorSelected(selectedColor) reset(componentContext.androidContext, dialog) } .flexGrow(1f)) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt index f7a0aa0f..fb412170 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt @@ -39,7 +39,7 @@ class FontSizeBottomSheet : LithoBottomSheet() { .textSizeDip(sEditorTextSize.toFloat()) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.note_option_font_size_example) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(CounterChooser.create(componentContext) .value(sEditorTextSize) .minValue(TEXT_SIZE_MIN) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 3953ba61..1d219db5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -37,18 +37,18 @@ class OpenSourceBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(openSourceDetails) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.osp_page_libraries) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.SECTION_HEADER))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 4f) .text(Markdown.render(LIBRARY_DETAILS_MD, true)) - .textColor(ApplicationBase.instance.themeController().get(ThemeColorType.TERTIARY_TEXT))) + .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.about_page_contribute) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index 43ed5083..d552c905 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -21,11 +21,10 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = componentContext.androidContext as MainActivity val options = ArrayList() - val flavor = ApplicationBase.instance.appFlavor() options.add(LithoOptionsItem( title = R.string.home_option_theme_color, subtitle = R.string.home_option_theme_color_subtitle, - icon = if (ApplicationBase.instance.themeController().isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, + icon = if (ApplicationBase.sAppTheme.isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, listener = { activity.performAction(MainActivityActions.COLOR_PICKER) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 40cccca1..78da6759 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -12,7 +12,7 @@ import com.maubis.scarlet.base.note.saveWithoutSync import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.KEY_LIST_VIEW import com.maubis.scarlet.base.support.ui.KEY_NIGHT_THEME import com.maubis.scarlet.base.support.ui.Theme -import com.maubis.scarlet.base.support.ui.sAppTheme +import com.maubis.scarlet.base.support.ui.sAppThemeLabel import com.maubis.scarlet.base.support.utils.getLastUsedAppVersionCode import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope @@ -31,7 +31,7 @@ class Migrator(val context: Context) { fun start() { runTask(KEY_MIGRATE_THEME) { val isNightMode = ApplicationBase.instance.store().get(KEY_NIGHT_THEME, true) - sAppTheme = if (isNightMode) Theme.DARK.name else Theme.LIGHT.name + sAppThemeLabel = if (isNightMode) Theme.DARK.name else Theme.LIGHT.name ApplicationBase.sAppTheme.notifyChange(context) } runTask(key = KEY_MIGRATE_REMINDERS) { @@ -63,7 +63,7 @@ class Migrator(val context: Context) { runTaskIf( getLastUsedAppVersionCode() == 0, KEY_MIGRATE_DEFAULT_VALUES) { - sAppTheme = Theme.DARK.name + sAppThemeLabel = Theme.DARK.name ApplicationBase.instance.store().put(KEY_LIST_VIEW, true) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt index aef207a9..b68ae4d0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt @@ -9,8 +9,8 @@ import com.maubis.scarlet.base.R class BottomSheetTabletDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); - val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width_for_tablets); - getWindow().setLayout( + val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width_for_tablets) + window?.setLayout( if (width > 0) width else ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index 0fc1ec61..a074e047 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -13,7 +13,7 @@ import com.maubis.scarlet.base.support.utils.throwOrReturn import java.lang.ref.WeakReference const val KEY_APP_THEME = "KEY_APP_THEME" -var sAppTheme: String +var sAppThemeLabel: String get() = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) set(value) = ApplicationBase.instance.store().put(KEY_APP_THEME, value) @@ -29,10 +29,10 @@ fun setThemeFromSystem(context: Context) { Configuration.UI_MODE_NIGHT_YES -> Theme.VERY_DARK.name else -> Theme.VERY_DARK.name } - if (systemBasedTheme === sAppTheme) { + if (systemBasedTheme === sAppThemeLabel) { return } - sAppTheme = systemBasedTheme + sAppThemeLabel = systemBasedTheme } // Old Theme Key, remove in future once theme is properly handled @@ -135,7 +135,7 @@ class ThemeManager : IThemeManager { fun getThemeFromStore(): Theme { return try { - Theme.valueOf(sAppTheme) + Theme.valueOf(sAppThemeLabel) } catch (exception: Exception) { throwOrReturn(exception, Theme.DARK) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index bc43e362..6510a237 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -61,9 +61,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { } fun setStatusBarColor(color: Int) { - if (OsVersionUtils.canSetStatusBarColor()) { - window.statusBarColor = color - } + window.statusBarColor = color } fun setStatusBarTextColor() { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt index 088eabd1..f9e6b472 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt @@ -21,11 +21,10 @@ object FlavorUtils { fun hasProAppInstalled(context: Context): Boolean { val reference = WeakReference(context) GlobalScope.launch(Dispatchers.IO) { - var found = false - try { - found = reference.get()?.packageManager?.getPackageInfo(PRO_APP_PACKAGE_NAME, 0) != null + val found = try { + reference.get()?.packageManager?.getPackageInfo(PRO_APP_PACKAGE_NAME, 0) != null } catch (e: Exception) { - found = throwOrReturn(e, false) + throwOrReturn(e, false) } ApplicationBase.instance.store().put(KEY_PRO_APP_INSTALLED, found) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt index 373e0d73..e8a14961 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/OsVersionUtils.kt @@ -3,10 +3,6 @@ package com.maubis.scarlet.base.support.utils import android.os.Build object OsVersionUtils { - fun canSetStatusBarColor() = Build.VERSION.SDK_INT >= 21 - - fun requiresTTSUtteranceId() = Build.VERSION.SDK_INT >= 21 - fun canExtractReferrer() = Build.VERSION.SDK_INT >= 22 fun requiresPermissions() = Build.VERSION.SDK_INT >= 23 diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index 86937cc7..1f1ab206 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -41,7 +41,7 @@ object PendingItemIconSpec { fun onCreate(context: ComponentContext, @Prop(resType = ResType.STRING) label: String, @Prop(resType = ResType.DRAWABLE) icon: Drawable): Component { - val secondaryColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val secondaryColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) return Row.create(context) .paddingDip(YogaEdge.HORIZONTAL, 8f) .paddingDip(YogaEdge.VERTICAL, 4f) From a7ba280837811e0492e1aa4c91d4d683c18f5904 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Wed, 30 Oct 2019 23:48:29 +0000 Subject: [PATCH 110/134] [Refactor] Cleaning up even more --- .../com/maubis/scarlet/base/MainActivity.kt | 12 ++++---- .../scarlet/base/MainActivityExtensions.kt | 3 +- .../scarlet/base/config/ApplicationBase.kt | 17 ++++++++--- .../maubis/scarlet/base/config/CoreConfig.kt | 2 -- .../scarlet/base/config/MaterialNoteConfig.kt | 6 ---- .../base/core/note/MaterialNoteActor.kt | 6 ++-- .../scarlet/base/core/note/NoteImage.kt | 12 ++++---- .../export/activity/ImportNoteActivity.kt | 12 ++++---- .../export/recycler/FileImportViewHolder.kt | 12 ++++---- .../base/export/remote/RemoteFolder.kt | 6 ++-- .../export/sheet/ExportNotesBottomSheet.kt | 3 +- .../sheet/ExternalFolderSyncBottomSheet.kt | 7 +++-- .../export/sheet/PermissionBottomSheet.kt | 3 +- .../base/export/support/ExternalFolderSync.kt | 13 ++++---- .../base/export/support/NoteExporter.kt | 18 +++++------ .../activity/OpenTextIntentOrFileActivity.kt | 7 +++-- .../main/recycler/InformationRecyclerItem.kt | 29 +++++++++--------- .../recycler/ToolbarMainRecyclerHolder.kt | 5 ++-- .../base/main/sheets/AlertBottomSheet.kt | 8 +++-- .../base/main/sheets/ExceptionBottomSheet.kt | 3 +- .../main/sheets/HomeOptionsBottomSheet.kt | 6 ++-- .../sheets/InstallProUpsellBottomSheet.kt | 3 +- .../base/main/sheets/WhatsNewBottomSheet.kt | 3 +- .../main/specs/MainActivityBottomBarSpec.kt | 5 ++-- .../scarlet/base/note/NoteExtensions.kt | 2 +- .../note/actions/TextToSpeechBottomSheet.kt | 5 ++-- .../creation/activity/CreateNoteActivity.kt | 6 ++-- .../activity/ShareToScarletRouterActivity.kt | 4 +-- .../creation/activity/ViewNoteActivity.kt | 17 ++++++----- .../sheet/EditorOptionsBottomSheet.kt | 26 ++++++++-------- .../creation/sheet/FormatActionBottomSheet.kt | 4 +-- .../creation/sheet/MarkdownHelpBottomSheet.kt | 3 +- .../sheet/CreateOrEditFolderBottomSheet.kt | 7 +++-- .../sheet/FolderChooserBottomSheetBase.kt | 6 ++-- .../formats/recycler/FormatImageViewHolder.kt | 6 ++-- .../formats/recycler/FormatViewHolderBase.kt | 30 +++++++++---------- .../recycler/NoteRecyclerViewHolderBase.kt | 4 +-- .../reminders/sheet/ReminderBottomSheet.kt | 7 +++-- .../activity/SelectableNotesActivityBase.kt | 10 ++++--- .../tag/sheet/CreateOrEditTagBottomSheet.kt | 7 +++-- .../base/notification/NotificationHandler.kt | 9 +++--- .../security/activity/AppLockActivitySpecs.kt | 13 ++++---- .../security/sheets/NoPincodeBottomSheet.kt | 8 +++-- .../security/sheets/PincodeBottomSheet.kt | 9 +++--- .../base/service/FloatingNoteService.kt | 11 ++++--- .../base/settings/sheet/AboutUsBottomSheet.kt | 11 +++---- .../settings/sheet/FontSizeBottomSheet.kt | 8 +++-- .../InternalSettingsOptionsBottomSheet.kt | 10 +++---- .../settings/sheet/LineCountBottomSheet.kt | 6 ++-- .../settings/sheet/OpenSourceBottomSheet.kt | 7 +++-- .../sheet/SecurityOptionsBottomSheet.kt | 18 +++++------ .../sheet/SortingOptionsBottomSheet.kt | 6 ++-- .../sheet/ThemeColorPickerBottomSheet.kt | 3 +- .../sheet/UISettingsOptionsBottomSheet.kt | 16 +++++----- .../base/support/database/MigrationUtils.kt | 12 ++++---- .../support/sheets/GridBottomSheetBase.kt | 3 +- .../base/support/sheets/LithoBottomSheet.kt | 7 +++-- .../sheets/LithoChooseOptionBottomSheet.kt | 4 +-- .../support/sheets/LithoOptionBottomSheet.kt | 11 ++++--- .../base/support/specs/BottomSheetBarSpec.kt | 5 ++-- .../base/support/specs/CounterChooserSpec.kt | 3 +- .../base/support/specs/GridSectionViewSpec.kt | 5 ++-- .../scarlet/base/support/specs/SpecUtils.kt | 5 ++-- .../scarlet/base/support/ui/CircleDrawable.kt | 3 +- .../base/support/ui/LithoCircleDrawable.kt | 3 +- .../scarlet/base/support/ui/ThemeManager.kt | 10 +++---- .../scarlet/base/support/ui/ThemedActivity.kt | 11 +++---- .../support/ui/ThemedBottomSheetFragment.kt | 11 +++---- .../base/support/utils/AppVersionUtils.kt | 14 ++++----- .../base/support/utils/ExceptionUtils.kt | 17 ++++++----- .../scarlet/base/support/utils/FlavorUtils.kt | 6 ++-- .../widget/sheet/WidgetOptionsBottomSheet.kt | 26 ++++++++-------- .../quicknote/database/RemoteController.kt | 8 ++--- .../quicknote/drive/GDriveActivitySpecs.kt | 15 +++++----- .../drive/GDriveLogoutActivitySpecs.kt | 9 +++--- .../drive/GDrivePendingBottomSheet.kt | 10 +++---- .../drive/GDriveRemoteImageFolder.kt | 4 +-- .../firebase/activity/DataPolicyActivity.kt | 5 ++-- .../activity/FirebaseActivitySpecs.kt | 13 ++++---- .../activity/FirebaseLoginActivity.kt | 4 +-- .../activity/FirebaseRemovalActivitySpecs.kt | 15 +++++----- .../firebase/support/FirebaseAuthenticator.kt | 4 +-- .../firebase/support/RemoteConfigFetcher.kt | 13 ++++---- 83 files changed, 394 insertions(+), 342 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index 85bb97ad..e4407d82 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -16,6 +16,8 @@ import com.facebook.litho.LithoView import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.note.NoteState @@ -98,7 +100,7 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { if (sAutomaticTheme) { setThemeFromSystem(this) } - ApplicationBase.sAppTheme.notifyChange(this) + sAppTheme.notifyChange(this) if (shouldShowWhatsNewSheet()) { openSheet(this, WhatsNewBottomSheet()) @@ -162,8 +164,8 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { val staggeredView = UISettingsOptionsBottomSheet.useGridView val isTablet = resources.getBoolean(R.bool.is_tablet) - val isMarkdownEnabled = ApplicationBase.instance.store().get(KEY_MARKDOWN_ENABLED, true) - val isMarkdownHomeEnabled = ApplicationBase.instance.store().get(KEY_MARKDOWN_HOME_ENABLED, true) + val isMarkdownEnabled = sAppPreferences.get(KEY_MARKDOWN_ENABLED, true) + val isMarkdownHomeEnabled = sAppPreferences.get(KEY_MARKDOWN_HOME_ENABLED, true) val adapterExtra = Bundle() adapterExtra.putBoolean(KEY_MARKDOWN_ENABLED, isMarkdownEnabled && isMarkdownHomeEnabled) adapterExtra.putInt(STORE_KEY_LINE_COUNT, sNoteItemLineCount) @@ -513,11 +515,9 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { override fun notifyThemeChange() { setSystemTheme() - - val theme = ApplicationBase.sAppTheme containerLayoutMain.setBackgroundColor(getThemeColor()) - val toolbarIconColor = theme.get(ThemeColorType.TOOLBAR_ICON) + val toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) deleteTrashIcon.setColorFilter(toolbarIconColor) deletesAutomatically.setTextColor(toolbarIconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt index 281da17d..dc30d3f7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -3,6 +3,7 @@ package com.maubis.scarlet.base import android.content.Context import android.content.Intent import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.settings.sheet.ThemeColorPickerBottomSheet import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.ui.sAppThemeLabel @@ -47,7 +48,7 @@ fun MainActivity.performAction(action: MainActivityActions) { this.onThemeChange = { theme -> if (sAppThemeLabel != theme.name) { sAppThemeLabel = theme.name - ApplicationBase.sAppTheme.notifyChange(activity) + sAppTheme.notifyChange(activity) activity.startActivity(MainActivityActions.COLOR_PICKER.intent(activity)) activity.finish() } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index f6159397..75043d56 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -3,6 +3,8 @@ package com.maubis.scarlet.base.config import android.app.Application import com.evernote.android.job.JobManager import com.facebook.soloader.SoLoader +import com.github.bijoysingh.starter.prefs.Store +import com.github.bijoysingh.starter.prefs.VersionedStore import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator @@ -15,6 +17,10 @@ import com.maubis.scarlet.base.support.utils.sDateFormat abstract class ApplicationBase : Application() { override fun onCreate() { super.onCreate() + + // Preferences + sAppPreferences = VersionedStore.get(this, "USER_PREFERENCES", 1) + sDateFormat = DateFormatUtils(this) SoLoader.init(this, false) try { @@ -22,10 +28,10 @@ abstract class ApplicationBase : Application() { } catch (exception: Exception) { maybeThrow(exception) } - noteImagesFolder = NoteImage(this) // Setup Image Cache - sImageCache = ImageCache(this) + sAppImageStorage = NoteImage(this) + sAppImageCache = ImageCache(this) // Setup Application Theme sAppTheme = ThemeManager() @@ -33,10 +39,13 @@ abstract class ApplicationBase : Application() { } companion object { - lateinit var noteImagesFolder: NoteImage lateinit var instance: CoreConfig - lateinit var sImageCache: ImageCache + lateinit var sAppImageStorage: NoteImage + lateinit var sAppImageCache: ImageCache + + lateinit var sAppPreferences: Store + lateinit var sAppTheme: ThemeManager var folderSync: FolderRemoteDatabase? = null diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 07e5ee90..1ba4377c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -60,8 +60,6 @@ abstract class CoreConfig(context: Context) { abstract fun appFlavor(): Flavor - abstract fun store(): Store - companion object { val notesDb get() = instance.notesDatabase() val tagsDb get() = instance.tagsDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 841e7eb0..3115c7c3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -24,16 +24,12 @@ import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.support.utils.Flavor -const val USER_PREFERENCES_STORE_NAME = "USER_PREFERENCES"; -const val USER_PREFERENCES_VERSION = 1; - open class MaterialNoteConfig(context: Context) : CoreConfig(context) { val db = AppDatabase.createDatabase(context) val notesProvider = NotesProvider() val tagsProvider = TagsProvider() val foldersProvider = FoldersProvider() - val store = VersionedStore.get(context, USER_PREFERENCES_STORE_NAME, USER_PREFERENCES_VERSION) override fun database(): AppDatabase = db @@ -63,6 +59,4 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun startListener(activity: AppCompatActivity) {} override fun appFlavor(): Flavor = Flavor.NONE - - override fun store(): Store = store } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index be789865..bfdcffe0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -9,7 +9,7 @@ import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.database.room.note.Note @@ -69,7 +69,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { } override fun offlineDelete(context: Context) { - noteImagesFolder.deleteAllFiles(note) + sAppImageStorage.deleteAllFiles(note) if (note.isUnsaved()) { return } @@ -108,7 +108,7 @@ open class MaterialNoteActor(val note: Note) : INoteActor { notifyAllChanged(context) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? notificationManager?.cancel(note.uid) - ApplicationBase.sImageCache.deleteNote(note.uuid) + ApplicationBase.sAppImageCache.deleteNote(note.uuid) } protected fun onNoteUpdated(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt index 8e2426a8..a2ef989c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt @@ -67,7 +67,7 @@ class NoteImage(context: Context) { return@launch } - val bitmap = ApplicationBase.sImageCache.loadFromCache(file) + val bitmap = ApplicationBase.sAppImageCache.loadFromCache(file) if (bitmap === null) { deleteIfExist(file) GlobalScope.launch(Dispatchers.Main) { @@ -88,8 +88,8 @@ class NoteImage(context: Context) { image: ImageView, callback: ImageLoadCallback? = null) { GlobalScope.launch { - val thumbnailFile = ApplicationBase.sImageCache.thumbnailFile(noteUUID, imageUuid) - val persistentFile = ApplicationBase.sImageCache.persistentFile(noteUUID, imageUuid) + val thumbnailFile = ApplicationBase.sAppImageCache.thumbnailFile(noteUUID, imageUuid) + val persistentFile = ApplicationBase.sAppImageCache.persistentFile(noteUUID, imageUuid) if (!persistentFile.exists()) { GlobalScope.launch(Dispatchers.Main) { @@ -100,7 +100,7 @@ class NoteImage(context: Context) { } if (thumbnailFile.exists()) { - val bitmap = ApplicationBase.sImageCache.loadFromCache(thumbnailFile) + val bitmap = ApplicationBase.sAppImageCache.loadFromCache(thumbnailFile) if (bitmap === null) { deleteIfExist(thumbnailFile) GlobalScope.launch(Dispatchers.Main) { @@ -117,7 +117,7 @@ class NoteImage(context: Context) { return@launch } - val persistentBitmap = ApplicationBase.sImageCache.loadFromCache(persistentFile) + val persistentBitmap = ApplicationBase.sAppImageCache.loadFromCache(persistentFile) if (persistentBitmap === null) { deleteIfExist(persistentFile) GlobalScope.launch(Dispatchers.Main) { @@ -127,7 +127,7 @@ class NoteImage(context: Context) { return@launch } - val compressedBitmap = ApplicationBase.sImageCache.saveThumbnail(thumbnailFile, persistentBitmap) + val compressedBitmap = ApplicationBase.sAppImageCache.saveThumbnail(thumbnailFile, persistentBitmap) GlobalScope.launch(Dispatchers.Main) { image.visibility = View.VISIBLE image.setImageBitmap(compressedBitmap) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt index 58015747..0fd02a95 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt @@ -10,6 +10,7 @@ import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.recycler.FileRecyclerItem import com.maubis.scarlet.base.export.support.NoteImporter import com.maubis.scarlet.base.note.recycler.NoteAppAdapter @@ -95,13 +96,12 @@ class ImportNoteActivity : SecuredActivity() { } override fun notifyThemeChange() { - val theme = ApplicationBase.sAppTheme - background.setBackgroundColor(theme.get(ThemeColorType.BACKGROUND)) - backButton.setColorFilter(theme.get(ThemeColorType.TOOLBAR_ICON)) - pageTitle.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) - importFile.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) + background.setBackgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + backButton.setColorFilter(sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) + pageTitle.setTextColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + importFile.setTextColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setBackgroundResource( - if (ApplicationBase.sAppTheme.isNightTheme()) R.drawable.light_circular_border_bg + if (sAppTheme.isNightTheme()) R.drawable.light_circular_border_bg else R.drawable.dark_circular_border_bg) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt index 59c4715e..1a5333e9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt @@ -10,6 +10,7 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -25,11 +26,10 @@ class FileImportViewHolder(context: Context, root: View) private val fileSize: TextView = findViewById(R.id.file_size) init { - val theme = ApplicationBase.sAppTheme - fileName.setTextColor(theme.get(ThemeColorType.SECONDARY_TEXT)) - filePath.setTextColor(theme.get(ThemeColorType.HINT_TEXT)) - fileDate.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) - fileSize.setTextColor(theme.get(ThemeColorType.TERTIARY_TEXT)) + fileName.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + filePath.setTextColor(sAppTheme.get(ThemeColorType.HINT_TEXT)) + fileDate.setTextColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + fileSize.setTextColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) } override fun populate(data: RecyclerItem, extra: Bundle?) { @@ -43,7 +43,7 @@ class FileImportViewHolder(context: Context, root: View) (context as ImportNoteActivity).select(item) } root.setBackgroundColor( - if (item.selected) ApplicationBase.sAppTheme.get( + if (item.selected) sAppTheme.get( context, R.color.material_grey_100, R.color.dark_hint_text) else Color.TRANSPARENT) } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt index ac444c2a..1d490b30 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt @@ -2,7 +2,7 @@ package com.maubis.scarlet.base.export.remote import com.github.bijoysingh.starter.util.FileManager import com.google.gson.Gson -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.export.support.KEY_EXTERNAL_FOLDER_SYNC_LAST_SCAN import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.Dispatchers @@ -23,7 +23,7 @@ class RemoteFolder(val folder: File, val deletedUuids = HashSet() val lastScanKey = "${KEY_EXTERNAL_FOLDER_SYNC_LAST_SCAN}_${folder.name}" - var lastScan = ApplicationBase.instance.store().get(lastScanKey, 0L) + var lastScan = sAppPreferences.get(lastScanKey, 0L) init { GlobalScope.launch(Dispatchers.IO) { @@ -52,7 +52,7 @@ class RemoteFolder(val folder: File, } onInitComplete() - ApplicationBase.instance.store().put(lastScanKey, System.currentTimeMillis()) + sAppPreferences.put(lastScanKey, System.currentTimeMillis()) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt index 31fbbdcf..2455524e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt @@ -13,6 +13,7 @@ import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.support.* import com.maubis.scarlet.base.support.sheets.* import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -52,7 +53,7 @@ class ExportNotesBottomSheet : LithoBottomSheet() { .text(filenameRender) .typeface(Typeface.MONOSPACE) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) getOptions(componentContext).forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index ef9ed699..ed79c12e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -10,6 +10,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.export.support.ExternalFolderSync import com.maubis.scarlet.base.export.support.sExternalFolderSync @@ -39,21 +40,21 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.import_export_layout_folder_sync_description) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .typeface(FONT_MONSERRAT) .textRes(R.string.import_export_layout_folder_sync_folder) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .text(sFolderSyncPath) .typeface(Typeface.MONOSPACE) .paddingDip(YogaEdge.HORIZONTAL, 20f) .paddingDip(YogaEdge.VERTICAL, 8f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(separatorSpec(componentContext).alpha(0.5f)) getOptions().forEach { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt index fea886da..fa6956c4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt @@ -8,6 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -29,7 +30,7 @@ class PermissionBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.permission_layout_give_permission_details) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.permission_layout_give_permission_ok) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index 81d369ca..ded6c021 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -8,6 +8,7 @@ import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.folderSync +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.export.data.ExportableFolder import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.export.data.ExportableTag @@ -26,16 +27,16 @@ const val KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED = "external_folder_sync_backup_ const val KEY_EXTERNAL_FOLDER_SYNC_PATH = "external_folder_sync_path" var sExternalFolderSync: Boolean - get() = ApplicationBase.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, false) - set(value) = ApplicationBase.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, value) + get() = sAppPreferences.get(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, false) + set(value) = sAppPreferences.put(KEY_EXTERNAL_FOLDER_SYNC_ENABLED, value) var sFolderSyncPath: String - get() = ApplicationBase.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_PATH, "$NOTES_EXPORT_FOLDER/Sync/") - set(value) = ApplicationBase.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_PATH, value) + get() = sAppPreferences.get(KEY_EXTERNAL_FOLDER_SYNC_PATH, "$NOTES_EXPORT_FOLDER/Sync/") + set(value) = sAppPreferences.put(KEY_EXTERNAL_FOLDER_SYNC_PATH, value) var sFolderSyncBackupLocked: Boolean - get() = ApplicationBase.instance.store().get(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, true) - set(value) = ApplicationBase.instance.store().put(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, value) + get() = sAppPreferences.get(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, true) + set(value) = sAppPreferences.put(KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED, value) object ExternalFolderSync { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt index c0b7f7d0..b2497dfe 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt @@ -4,7 +4,7 @@ import android.os.AsyncTask import android.os.Environment import com.github.bijoysingh.starter.util.FileManager import com.google.gson.Gson -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb @@ -26,18 +26,18 @@ const val AUTO_BACKUP_INTERVAL_MS = 1000 * 60 * 60 * 6 // 6 hours update const val STORE_KEY_BACKUP_MARKDOWN = "KEY_BACKUP_MARKDOWN" var sBackupMarkdown: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_BACKUP_MARKDOWN, false) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_BACKUP_MARKDOWN, value) + get() = sAppPreferences.get(STORE_KEY_BACKUP_MARKDOWN, false) + set(value) = sAppPreferences.put(STORE_KEY_BACKUP_MARKDOWN, value) const val STORE_KEY_BACKUP_LOCKED = "KEY_BACKUP_LOCKED" var sBackupLockedNotes: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_BACKUP_LOCKED, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_BACKUP_LOCKED, value) + get() = sAppPreferences.get(STORE_KEY_BACKUP_LOCKED, true) + set(value) = sAppPreferences.put(STORE_KEY_BACKUP_LOCKED, value) const val STORE_KEY_AUTO_BACKUP_MODE = "KEY_AUTO_BACKUP_MODE" var sAutoBackupMode: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_AUTO_BACKUP_MODE, false) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_AUTO_BACKUP_MODE, value) + get() = sAppPreferences.get(STORE_KEY_AUTO_BACKUP_MODE, false) + set(value) = sAppPreferences.put(STORE_KEY_AUTO_BACKUP_MODE, value) class NoteExporter() { @@ -72,7 +72,7 @@ class NoteExporter() { if (!sAutoBackupMode) { return@execute } - val lastBackup = ApplicationBase.instance.store().get(KEY_AUTO_BACKUP_LAST_TIMESTAMP, 0L) + val lastBackup = sAppPreferences.get(KEY_AUTO_BACKUP_LAST_TIMESTAMP, 0L) val lastTimestamp = notesDb.getLastTimestamp() if (lastBackup + AUTO_BACKUP_INTERVAL_MS >= lastTimestamp) { return@execute @@ -84,7 +84,7 @@ class NoteExporter() { return@execute } saveToFile(exportFile, getExportContent()) - ApplicationBase.instance.store() + sAppPreferences .put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 1203c7e3..1c0d487b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -17,6 +17,7 @@ import com.maubis.markdown.spannable.clearMarkdownSpans import com.maubis.markdown.spannable.setFormats import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.support.NoteImporter @@ -142,15 +143,15 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { val containerLayout = findViewById(R.id.container_layout); containerLayout.setBackgroundColor(getThemeColor()); - val toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON); + val toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON); backButton.setColorFilter(toolbarIconColor) - val textColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val textColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) filename.setTextColor(textColor) title.setTextColor(textColor) content.setTextColor(textColor) - val actionColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + val actionColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) actionDone.setImageTint(actionColor) actionDone.setTextColor(actionColor) } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt index 2b2bf8d1..670fa2a0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt @@ -7,6 +7,7 @@ import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.export.sheet.BackupSettingsOptionsBottomSheet import com.maubis.scarlet.base.export.support.NoteExporter import com.maubis.scarlet.base.main.activity.INTENT_KEY_DIRECT_NOTES_TRANSFER @@ -51,7 +52,7 @@ fun getAppUpdateInformationItem(context: Context): InformationRecyclerItem { fun shouldShowReviewInformationItem(): Boolean { return probability(0.01f) - && !ApplicationBase.instance.store().get(KEY_INFO_RATE_AND_REVIEW, false) + && !sAppPreferences.get(KEY_INFO_RATE_AND_REVIEW, false) } fun getReviewInformationItem(context: Context): InformationRecyclerItem { @@ -60,14 +61,14 @@ fun getReviewInformationItem(context: Context): InformationRecyclerItem { R.string.home_option_rate_and_review, R.string.home_option_rate_and_review_subtitle ) { - ApplicationBase.instance.store().put(KEY_INFO_RATE_AND_REVIEW, true) + sAppPreferences.put(KEY_INFO_RATE_AND_REVIEW, true) IntentUtils.openAppPlayStore(context) } } fun shouldShowThemeInformationItem(): Boolean { return probability(0.01f) - && !ApplicationBase.instance.store().get(KEY_THEME_OPTIONS, false) + && !sAppPreferences.get(KEY_THEME_OPTIONS, false) } fun getThemeInformationItem(activity: MainActivity): InformationRecyclerItem { @@ -76,14 +77,14 @@ fun getThemeInformationItem(activity: MainActivity): InformationRecyclerItem { R.string.home_option_ui_experience, R.string.home_option_ui_experience_subtitle ) { - ApplicationBase.instance.store().put(KEY_THEME_OPTIONS, true) + sAppPreferences.put(KEY_THEME_OPTIONS, true) UISettingsOptionsBottomSheet.openSheet(activity) } } fun shouldShowBackupInformationItem(): Boolean { return probability(0.01f) - && !ApplicationBase.instance.store().get(KEY_BACKUP_OPTIONS, false) + && !sAppPreferences.get(KEY_BACKUP_OPTIONS, false) } fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { @@ -92,7 +93,7 @@ fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { R.string.home_option_backup_options, R.string.home_option_backup_options_subtitle ) { - ApplicationBase.instance.store().put(KEY_BACKUP_OPTIONS, true) + sAppPreferences.put(KEY_BACKUP_OPTIONS, true) openSheet(activity, BackupSettingsOptionsBottomSheet()) } } @@ -100,7 +101,7 @@ fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { fun shouldShowInstallProInformationItem(): Boolean { return probability(0.01f) - && ApplicationBase.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT + && sAppPreferences.get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT && !FlavorUtils.isPro() } @@ -119,12 +120,12 @@ fun shouldShowSignInformationItem(context: Context): Boolean { if (ApplicationBase.instance.authenticator().isLoggedIn(context) || FlavorUtils.isOpenSource()) { return false } - if (ApplicationBase.instance.store().get(KEY_FORCE_SHOW_SIGN_IN, false)) { - ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, false) + if (sAppPreferences.get(KEY_FORCE_SHOW_SIGN_IN, false)) { + sAppPreferences.put(KEY_FORCE_SHOW_SIGN_IN, false) return true } return probability(0.01f) - && !ApplicationBase.instance.store().get(KEY_INFO_SIGN_IN, false) + && !sAppPreferences.get(KEY_INFO_SIGN_IN, false) } fun getSignInInformationItem(context: Context): InformationRecyclerItem { @@ -139,15 +140,15 @@ fun getSignInInformationItem(context: Context): InformationRecyclerItem { } fun notifyProUpsellShown() { - val proUpsellCount = ApplicationBase.instance.store().get(KEY_INFO_INSTALL_PRO_v2, 0) - ApplicationBase.instance.store().put(KEY_INFO_INSTALL_PRO_v2, proUpsellCount + 1) + val proUpsellCount = sAppPreferences.get(KEY_INFO_INSTALL_PRO_v2, 0) + sAppPreferences.put(KEY_INFO_INSTALL_PRO_v2, proUpsellCount + 1) } fun shouldShowMigrateToProAppInformationItem(context: Context): Boolean { return FlavorUtils.isLite() && FlavorUtils.hasProAppInstalled(context) - && !ApplicationBase.instance.store().get(KEY_MIGRATE_TO_PRO_SUCCESS, false) + && !sAppPreferences.get(KEY_MIGRATE_TO_PRO_SUCCESS, false) } fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem { @@ -164,7 +165,7 @@ fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem .setPackage(FlavorUtils.PRO_APP_PACKAGE_NAME) try { context.startActivity(intent) - ApplicationBase.instance.store().put(KEY_MIGRATE_TO_PRO_SUCCESS, true) + sAppPreferences.put(KEY_MIGRATE_TO_PRO_SUCCESS, true) } catch (exception: Exception) { ToastHelper.show(context, "Failed transferring notes to Scarlet Pro") maybeThrow(exception) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index 6059de0c..5fab3154 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -12,6 +12,7 @@ import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.settings.sheet.InternalSettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -37,10 +38,10 @@ class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerView SettingsOptionsBottomSheet.openSheet((context as MainActivity)) } - val titleColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) toolbarTitle.setTextColor(titleColor) - val toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val toolbarIconColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) toolbarIconSearch.setColorFilter(toolbarIconColor) toolbarIconSettings.setColorFilter(toolbarIconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt index d6d5ae74..381fb39a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt @@ -9,6 +9,8 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.note.NoteState @@ -60,7 +62,7 @@ class AlertBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(config.description) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(config.positiveText) .onPrimaryClick { @@ -105,8 +107,8 @@ fun openDeleteFormatDialog(activity: ViewAdvancedNoteActivity, format: Format) { const val STORE_KEY_IMAGE_SYNC_NOTICE = "IMAGE_SYNC_NOTICE" var sImageSyncNoticeShown: Int - get() = ApplicationBase.instance.store().get(STORE_KEY_IMAGE_SYNC_NOTICE, 0) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_IMAGE_SYNC_NOTICE, value) + get() = sAppPreferences.get(STORE_KEY_IMAGE_SYNC_NOTICE, 0) + set(value) = sAppPreferences.put(STORE_KEY_IMAGE_SYNC_NOTICE, value) fun openImageNotSynced(activity: ThemedActivity) { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt index 6521690b..8b0b75e5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt @@ -12,6 +12,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -32,7 +33,7 @@ class ExceptionBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_small) .text(Markdown.render("```\n${Log.getStackTraceString(exception)}\n```", true)) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.exception_sheet_crash_app) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt index 50427896..444fede6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.tag.TagBuilder import com.maubis.scarlet.base.database.room.tag.Tag @@ -37,9 +38,8 @@ class LithoTagOptionsItem( object TagItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoTagOptionsItem): Component { - val theme = ApplicationBase.sAppTheme - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) - val selectedColor = when (theme.isNightTheme()) { + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val selectedColor = when (sAppTheme.isNightTheme()) { true -> context.getColor(R.color.material_blue_400) false -> context.getColor(R.color.material_blue_700) } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index 310f0746..75286ae1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -9,6 +9,7 @@ import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -41,7 +42,7 @@ class InstallProUpsellBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.why_install_pro) .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(GridSectionView.create(componentContext) .maxLines(3) .numColumns(2) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 10d942ce..bf19b391 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -8,6 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -45,7 +46,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.whats_new_sheet_subtitle) .typeface(FONT_MONSERRAT) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(GridSectionView.create(componentContext) .maxLines(3) .numColumns(2) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 62b95fd7..1a60d54d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -19,6 +19,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT_MEDIUM import com.maubis.scarlet.base.core.folder.FolderBuilder @@ -203,8 +204,8 @@ object MainActivitySyncingNowSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop isSyncHappening: Boolean): Component { val colorConfig = ToolbarColorConfig( - toolbarBackgroundColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), - toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + toolbarBackgroundColor = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), + toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) ) val syncText = when (isSyncHappening) { true -> R.string.home_syncing_top_layout diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index f950ad5e..5501a243 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -301,7 +301,7 @@ fun Note.hasImages(): Boolean { fun Note.shareImages(context: Context) { val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } val bitmaps = imageFormats - .map { ApplicationBase.noteImagesFolder.getFile(uuid, it.text) } + .map { ApplicationBase.sAppImageStorage.getFile(uuid, it.text) } .filter { it.exists() } .map { BitmapHelper.loadFromFile(it) } .filterNotNull() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index 8bd467e1..fa74c846 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -7,6 +7,7 @@ import android.widget.TextView import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.getFullText import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -34,10 +35,10 @@ class TextToSpeechBottomSheet : ThemedBottomSheetFragment() { val nonNullNote = note!! val title = dialog.findViewById(R.id.options_title) - title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + title.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) val speakPlayPause = dialog.findViewById(R.id.speak_play_pause) - speakPlayPause.setColorFilter(ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) + speakPlayPause.setColorFilter(sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) speakPlayPause.setOnClickListener { val tts = textToSpeech if (tts === null) { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index fb751672..a1dba26f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -9,7 +9,7 @@ import android.support.v7.widget.helper.ItemTouchHelper import com.facebook.litho.ComponentContext import com.facebook.litho.LithoView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder @@ -137,7 +137,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { return } - val targetFile = noteImagesFolder.renameOrCopy(note!!, imageFile) + val targetFile = sAppImageStorage.renameOrCopy(note!!, imageFile) val index = getFormatIndex(type) triggerImageLoaded(index, targetFile) } @@ -300,7 +300,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { val formatToChange = formats[position] if (!formatToChange.text.isBlank()) { - val noteImage = noteImagesFolder + val noteImage = sAppImageStorage deleteIfExist(noteImage.getFile(note!!.uuid, formatToChange.text)) } formatToChange.text = file.name diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt index 4819df8b..e5d7beba 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -7,7 +7,7 @@ import android.os.Bundle import android.os.Parcelable import android.support.v7.app.AppCompatActivity import com.maubis.scarlet.base.MainActivity -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType @@ -72,7 +72,7 @@ class ShareToScarletRouterActivity : AppCompatActivity() { val temporaryImage = createTempFile() BitmapHelper.saveToFile(temporaryImage, bitmap) - images.add(noteImagesFolder.renameOrCopy(note, temporaryImage)) + images.add(sAppImageStorage.renameOrCopy(note, temporaryImage)) temporaryImage.delete() } catch (exception: Exception) { } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 31acf9db..9a212108 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -13,6 +13,8 @@ import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewControllerIte import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder @@ -151,8 +153,8 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit val currentNote = note val bundle = Bundle() bundle.putBoolean(KEY_EDITABLE, editModeValue) - bundle.putBoolean(KEY_MARKDOWN_ENABLED, ApplicationBase.instance.store().get(KEY_MARKDOWN_ENABLED, true)) - bundle.putBoolean(KEY_NIGHT_THEME, ApplicationBase.sAppTheme.isNightTheme()) + bundle.putBoolean(KEY_MARKDOWN_ENABLED, sAppPreferences.get(KEY_MARKDOWN_ENABLED, true)) + bundle.putBoolean(KEY_NIGHT_THEME, sAppTheme.isNightTheme()) bundle.putInt(STORE_KEY_TEXT_SIZE, sEditorTextSize) bundle.putInt(KEY_NOTE_COLOR, currentNote?.color ?: sNoteDefaultColor) bundle.putString(INTENT_KEY_NOTE_ID, currentNote?.uuid ?: generateUUID()) @@ -257,23 +259,22 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit return } - val theme = ApplicationBase.sAppTheme when { !useNoteColorAsBackground -> { - colorConfig.backgroundColor = theme.get(ThemeColorType.BACKGROUND) - colorConfig.toolbarIconColor = theme.get(ThemeColorType.TOOLBAR_ICON) + colorConfig.backgroundColor = sAppTheme.get(ThemeColorType.BACKGROUND) + colorConfig.toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) colorConfig.statusBarColor = colorConfig.backgroundColor - colorConfig.toolbarBackgroundColor = theme.get(ThemeColorType.TOOLBAR_BACKGROUND) + colorConfig.toolbarBackgroundColor = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND) } ColorUtil.isLightColored(currentNote.color) -> { colorConfig.backgroundColor = currentNote.color - colorConfig.toolbarIconColor = theme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) + colorConfig.toolbarIconColor = sAppTheme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) colorConfig.statusBarColor = darkerColor(currentNote.color) colorConfig.toolbarBackgroundColor = colorConfig.statusBarColor } else -> { colorConfig.backgroundColor = currentNote.color - colorConfig.toolbarIconColor = theme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) + colorConfig.toolbarIconColor = sAppTheme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) colorConfig.statusBarColor = darkerColor(currentNote.color) colorConfig.toolbarBackgroundColor = colorConfig.statusBarColor } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt index b982a9a2..9f554788 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.settings.sheet.ColorPickerBottomSheet import com.maubis.scarlet.base.settings.sheet.ColorPickerDefaultController import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet @@ -19,28 +19,28 @@ const val STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES = "editor_move_handles" const val STORE_KEY_NOTE_DEFAULT_COLOR = "KEY_NOTE_DEFAULT_COLOR" var sEditorLiveMarkdown: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, value) + get() = sAppPreferences.get(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, true) + set(value) = sAppPreferences.put(STORE_KEY_EDITOR_OPTIONS_LIVE_MARKDOWN, value) var sEditorMoveChecked: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS, value) + get() = sAppPreferences.get(STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS, true) + set(value) = sAppPreferences.put(STORE_KEY_EDITOR_OPTIONS_MOVE_CHECKED_ITEMS, value) var sEditorMarkdownDefault: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, false) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, value) + get() = sAppPreferences.get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, false) + set(value) = sAppPreferences.put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_DEFAULT, value) var sEditorMoveHandles: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, value) + get() = sAppPreferences.get(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, true) + set(value) = sAppPreferences.put(STORE_KEY_EDITOR_OPTIONS_MOVE_HANDLES, value) var sEditorMarkdownEnabled: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, value) + get() = sAppPreferences.get(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, true) + set(value) = sAppPreferences.put(STORE_KEY_EDITOR_OPTIONS_MARKDOWN_ENABLED, value) var sNoteDefaultColor: Int - get() = ApplicationBase.instance.store().get(STORE_KEY_NOTE_DEFAULT_COLOR, (0xFFD32F2F).toInt()) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_NOTE_DEFAULT_COLOR, value) + get() = sAppPreferences.get(STORE_KEY_NOTE_DEFAULT_COLOR, (0xFFD32F2F).toInt()) + set(value) = sAppPreferences.put(STORE_KEY_NOTE_DEFAULT_COLOR, value) class EditorOptionsBottomSheet : LithoOptionBottomSheet() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt index 6a3dd3be..81cb611e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt @@ -5,7 +5,7 @@ import com.facebook.litho.ComponentContext import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.note.NoteImage.Companion.deleteIfExist @@ -76,7 +76,7 @@ class FormatActionBottomSheet : GridOptionBottomSheet() { listener = { activity.deleteFormat(format) if (format.formatType === FormatType.IMAGE && !format.text.isBlank()) { - deleteIfExist(noteImagesFolder.getFile(noteUUID, format)) + deleteIfExist(sAppImageStorage.getFile(noteUUID, format)) } dismiss() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt index b561cf94..b6b76cc8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt @@ -9,6 +9,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -28,7 +29,7 @@ class MarkdownHelpBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_normal) .marginDip(YogaEdge.HORIZONTAL, 20f) .paddingDip(YogaEdge.VERTICAL, 4f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) } return column.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt index ffcdc16f..117521a2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt @@ -11,6 +11,7 @@ import android.widget.TextView import com.google.android.flexbox.FlexboxLayout import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.folder.isUnsaved import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.note.folder.delete @@ -50,9 +51,9 @@ class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { val removeBtn = dialog.findViewById(R.id.action_remove_button) val colorFlexbox = dialog.findViewById(R.id.color_flexbox) - title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - enterFolder.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - enterFolder.setHintTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.HINT_TEXT)) + title.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterFolder.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterFolder.setHintTextColor(sAppTheme.get(ThemeColorType.HINT_TEXT)) title.setText(if (folder.isUnsaved()) R.string.folder_sheet_add_note else R.string.folder_sheet_edit_note) action.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt index 647086a2..d7229499 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt @@ -14,6 +14,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder @@ -34,9 +35,8 @@ data class FolderOptionsItem( object FolderItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: FolderOptionsItem): Component { - val theme = ApplicationBase.sAppTheme - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) - val selectedColor = when (theme.isNightTheme()) { + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val selectedColor = when (sAppTheme.isNightTheme()) { true -> context.getColor(R.color.material_blue_400) false -> context.getColor(R.color.material_blue_700) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt index 5f099d80..565cd023 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatImageViewHolder.kt @@ -9,7 +9,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.util.ToastHelper import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.note.ImageLoadCallback import com.maubis.scarlet.base.main.sheets.openDeleteFormatDialog @@ -85,7 +85,7 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase when { fileName.isBlank() -> image.visibility = View.GONE else -> { - val file = noteImagesFolder.getFile(config.noteUUID, data) + val file = sAppImageStorage.getFile(config.noteUUID, data) when (file.exists()) { true -> populateFile(file) false -> { @@ -100,7 +100,7 @@ class FormatImageViewHolder(context: Context, view: View) : FormatViewHolderBase } fun populateFile(file: File) { - noteImagesFolder.loadPersistentFileToImageView(image, file, object : ImageLoadCallback { + sAppImageStorage.loadPersistentFileToImageView(image, file, object : ImageLoadCallback { override fun onSuccess() { noImageMessage.visibility = View.GONE } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index 1bbebbb7..518ef901 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -7,6 +7,7 @@ import android.view.View import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID @@ -46,29 +47,28 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView val tertiaryTextColor: Int val iconColor: Int val hintTextColor: Int - val theme = ApplicationBase.sAppTheme val isLightBackground = ColorUtil.isLightColored(noteColor) val linkColor: Int when { !useNoteColorAsBackground -> { - secondaryTextColor = theme.get(ThemeColorType.SECONDARY_TEXT) - tertiaryTextColor = theme.get(ThemeColorType.TERTIARY_TEXT) - iconColor = theme.get(ThemeColorType.TOOLBAR_ICON) - hintTextColor = theme.get(ThemeColorType.HINT_TEXT) - linkColor = theme.get(ThemeColorType.ACCENT_TEXT) + secondaryTextColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + tertiaryTextColor = sAppTheme.get(ThemeColorType.TERTIARY_TEXT) + iconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + hintTextColor = sAppTheme.get(ThemeColorType.HINT_TEXT) + linkColor = sAppTheme.get(ThemeColorType.ACCENT_TEXT) } isLightBackground -> { - secondaryTextColor = theme.get(context, Theme.LIGHT, ThemeColorType.SECONDARY_TEXT) - tertiaryTextColor = theme.get(context, Theme.LIGHT, ThemeColorType.TERTIARY_TEXT) - iconColor = theme.get(context, Theme.LIGHT, ThemeColorType.TOOLBAR_ICON) - hintTextColor = theme.get(context, Theme.LIGHT, ThemeColorType.HINT_TEXT) + secondaryTextColor = sAppTheme.get(context, Theme.LIGHT, ThemeColorType.SECONDARY_TEXT) + tertiaryTextColor = sAppTheme.get(context, Theme.LIGHT, ThemeColorType.TERTIARY_TEXT) + iconColor = sAppTheme.get(context, Theme.LIGHT, ThemeColorType.TOOLBAR_ICON) + hintTextColor = sAppTheme.get(context, Theme.LIGHT, ThemeColorType.HINT_TEXT) linkColor = ContextCompat.getColor(context, R.color.colorAccentYellowLight) } else -> { - secondaryTextColor = theme.get(context, Theme.DARK, ThemeColorType.SECONDARY_TEXT) - tertiaryTextColor = theme.get(context, Theme.DARK, ThemeColorType.TERTIARY_TEXT) - iconColor = theme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) - hintTextColor = theme.get(context, Theme.DARK, ThemeColorType.HINT_TEXT) + secondaryTextColor = sAppTheme.get(context, Theme.DARK, ThemeColorType.SECONDARY_TEXT) + tertiaryTextColor = sAppTheme.get(context, Theme.DARK, ThemeColorType.TERTIARY_TEXT) + iconColor = sAppTheme.get(context, Theme.DARK, ThemeColorType.TOOLBAR_ICON) + hintTextColor = sAppTheme.get(context, Theme.DARK, ThemeColorType.HINT_TEXT) linkColor = ContextCompat.getColor(context, R.color.colorAccentYellowDark) } } @@ -90,7 +90,7 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView } }(), backgroundColor = when (data.formatType) { - FormatType.CODE, FormatType.IMAGE -> ApplicationBase.sAppTheme.get(context, R.color.code_light, R.color.code_dark) + FormatType.CODE, FormatType.IMAGE -> sAppTheme.get(context, R.color.code_light, R.color.code_dark) else -> ContextCompat.getColor(context, R.color.transparent) }, secondaryTextColor = secondaryTextColor, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt index 127ce2b6..25e87fca 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt @@ -11,7 +11,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.isNoteLockedButAppUnlocked @@ -83,7 +83,7 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi val isImageAvailable = !note.imageSource.isBlank() image.visibility = visibility(isImageAvailable) if (isImageAvailable) { - noteImagesFolder.loadThumbnailFileToImageView(note.note.uuid, note.imageSource, image) + sAppImageStorage.loadThumbnailFileToImageView(note.note.uuid, note.imageSource, image) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt index 9595f119..5b95a83f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt @@ -9,6 +9,7 @@ import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.uibasics.views.UIActionView import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.note.Reminder import com.maubis.scarlet.base.core.note.ReminderInterval import com.maubis.scarlet.base.core.note.getReminderV2 @@ -208,9 +209,9 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { val reminderTime = dialog.findViewById(R.id.reminder_time) val reminderRepeat = dialog.findViewById(R.id.reminder_repeat) - val iconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) - val textColor = ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT) - val titleColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER) + val iconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + val textColor = sAppTheme.get(ThemeColorType.TERTIARY_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECTION_HEADER) reminderDate.setTitleColor(titleColor) reminderDate.setSubtitleColor(textColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index 3f283c22..b6cab84e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -13,6 +13,8 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.note.sort import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.main.recycler.EmptyRecyclerItem @@ -89,11 +91,11 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct open fun getLayoutUI(): Int = R.layout.activity_select_note fun setupRecyclerView() { - val staggeredView = ApplicationBase.instance.store().get(UISettingsOptionsBottomSheet.KEY_LIST_VIEW, false) + val staggeredView = sAppPreferences.get(UISettingsOptionsBottomSheet.KEY_LIST_VIEW, false) val isTablet = resources.getBoolean(R.bool.is_tablet) - val isMarkdownEnabled = ApplicationBase.instance.store().get(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) - val isMarkdownHomeEnabled = ApplicationBase.instance.store().get(SettingsOptionsBottomSheet.KEY_MARKDOWN_HOME_ENABLED, true) + val isMarkdownEnabled = sAppPreferences.get(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) + val isMarkdownHomeEnabled = sAppPreferences.get(SettingsOptionsBottomSheet.KEY_MARKDOWN_HOME_ENABLED, true) val adapterExtra = Bundle() adapterExtra.putBoolean(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, isMarkdownEnabled && isMarkdownHomeEnabled) adapterExtra.putInt(STORE_KEY_LINE_COUNT, sNoteItemLineCount) @@ -113,7 +115,7 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct val containerLayout = findViewById(R.id.container_layout) containerLayout.setBackgroundColor(getThemeColor()) - val toolbarIconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON); + val toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON); findViewById(R.id.back_button).setColorFilter(toolbarIconColor) findViewById(R.id.toolbar_title).setTextColor(toolbarIconColor) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt index 56e941b1..06b1dcff 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt @@ -7,6 +7,7 @@ import android.widget.EditText import android.widget.TextView import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.tag.isUnsaved import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.note.tag.delete @@ -43,9 +44,9 @@ class CreateOrEditTagBottomSheet : ThemedBottomSheetFragment() { val enterTag = dialog.findViewById(R.id.enter_tag) val removeBtn = dialog.findViewById(R.id.action_remove_button) - title.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - enterTag.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - enterTag.setHintTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.HINT_TEXT)) + title.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterTag.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + enterTag.setHintTextColor(sAppTheme.get(ThemeColorType.HINT_TEXT)) title.setText(if (tag.isUnsaved()) R.string.tag_sheet_create_title else R.string.tag_sheet_edit_title) action.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt index 2c613b75..ed25f0bc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt @@ -12,6 +12,7 @@ import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID @@ -95,16 +96,16 @@ class NotificationHandler(val context: Context) { contentView.setTextViewText(R.id.description, config.note.getTextForSharing()) contentView.setTextViewText(R.id.timestamp, config.note.getDisplayTime()) - val titleColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) - val descColor = ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val descColor = sAppTheme.get(ThemeColorType.TERTIARY_TEXT) contentView.setTextColor(R.id.title, titleColor) contentView.setTextColor(R.id.description, titleColor) contentView.setTextColor(R.id.timestamp, descColor) - val backgroundColor = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) + val backgroundColor = sAppTheme.get(ThemeColorType.BACKGROUND) contentView.setInt(R.id.root_layout, "setBackgroundColor", backgroundColor) - val iconColor = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + val iconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) contentView.setInt(R.id.options_button, "setColorFilter", iconColor) contentView.setInt(R.id.copy_button, "setColorFilter", iconColor) contentView.setInt(R.id.share_button, "setColorFilter", iconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt index 6f47916f..1c440d19 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -27,7 +28,7 @@ object AppLockViewSpec { @Prop onTextChange: (String) -> Unit, @Prop onClick: () -> Unit): Component { return Column.create(context) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(AppLockContentView.create(context) .fingerprintEnabled(fingerprintEnabled) .onTextChange(onTextChange) @@ -78,21 +79,21 @@ object AppLockContentViewSpec { else -> R.string.app_lock_details_no_fingerprint } val editBackground = when { - ApplicationBase.sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg + sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg else -> R.drawable.secondary_rounded_bg } return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.app_lock_title) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(description) .typeface(CoreConfig.FONT_MONSERRAT)) .child(EmptySpec.create(context).flexGrow(1f)) @@ -106,7 +107,7 @@ object AppLockContentViewSpec { .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) .imeOptions(EditorInfo.IME_ACTION_DONE) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt index fca47348..96198ccc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt @@ -8,6 +8,8 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -17,8 +19,8 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity const val STORE_KEY_NO_PIN_ASK = "KEY_NO_PIN_ASK" var sNoPinSetupNoticeShown: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_NO_PIN_ASK, false) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_NO_PIN_ASK, value) + get() = sAppPreferences.get(STORE_KEY_NO_PIN_ASK, false) + set(value) = sAppPreferences.put(STORE_KEY_NO_PIN_ASK, value) class NoPincodeBottomSheet : LithoBottomSheet() { var onSuccess: () -> Unit = {} @@ -36,7 +38,7 @@ class NoPincodeBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.no_pincode_sheet_details) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.no_pincode_sheet_set_up) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index f8b3efde..c831f93c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -18,6 +18,7 @@ import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.security.controller.PinLockController import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled @@ -61,7 +62,7 @@ object PincodeSheetViewSpec { @Prop data: PincodeSheetData, @Prop dismiss: () -> Unit): Component { val editBackground = when { - ApplicationBase.sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg + sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg else -> R.drawable.secondary_rounded_bg } @@ -76,7 +77,7 @@ object PincodeSheetViewSpec { .textSizeRes(R.dimen.font_size_large) .textRes(R.string.app_lock_details) .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(EditText.create(context) .backgroundRes(editBackground) .textSizeRes(R.dimen.font_size_xlarge) @@ -87,7 +88,7 @@ object PincodeSheetViewSpec { .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) .marginDip(YogaEdge.VERTICAL, 8f) @@ -114,7 +115,7 @@ object PincodeSheetViewSpec { when { data.isRemoveButtonEnabled -> Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.HINT_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.HINT_TEXT)) .textRes(R.string.security_sheet_button_remove) .textAlignment(Layout.Alignment.ALIGN_CENTER) .paddingDip(YogaEdge.VERTICAL, 12f) diff --git a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt index 95deb19b..7a44ed34 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt @@ -14,6 +14,7 @@ import com.bsk.floatingbubblelib.FloatingBubbleService import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.database.room.note.Note @@ -36,7 +37,6 @@ class FloatingNoteService : FloatingBubbleService() { private lateinit var panel: View override fun getConfig(): FloatingBubbleConfig { - val theme = ApplicationBase.sAppTheme return FloatingBubbleConfig.Builder() .bubbleIcon(ContextCompat.getDrawable(context, R.drawable.app_icon)) .removeBubbleIcon(ContextCompat.getDrawable( @@ -47,8 +47,8 @@ class FloatingNoteService : FloatingBubbleService() { .paddingDp(8) .borderRadiusDp(4) .physicsEnabled(true) - .expandableColor(theme.get(ThemeColorType.BACKGROUND)) - .triangleColor(theme.get(ThemeColorType.BACKGROUND)) + .expandableColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .triangleColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .gravity(Gravity.END) .expandableView(loadView()) .removeBubbleAlpha(0.7f) @@ -69,13 +69,12 @@ class FloatingNoteService : FloatingBubbleService() { stopSelf() } - val theme = ApplicationBase.sAppTheme val rootView = getInflater().inflate(R.layout.layout_add_note_overlay, null) description = rootView.findViewById(R.id.description) as TextView timestamp = rootView.findViewById(R.id.timestamp) as TextView - description.setTextColor(theme.get(ThemeColorType.SECONDARY_TEXT)) + description.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) val noteItem = note!! @@ -108,7 +107,7 @@ class FloatingNoteService : FloatingBubbleService() { } panel = rootView.findViewById(R.id.panel_layout) - panel.setBackgroundColor(theme.get(ThemeColorType.BACKGROUND)) + panel.setBackgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) setNote(noteItem) return rootView diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index 932c08e0..7075d712 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -10,6 +10,7 @@ import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -45,29 +46,29 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutUsDetails) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_about_app) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutAppDetails) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_app_version) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(version) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.about_page_rate) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt index fb412170..ac0e868b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt @@ -9,6 +9,8 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -21,8 +23,8 @@ const val TEXT_SIZE_MIN = 12 const val TEXT_SIZE_MAX = 24 var sEditorTextSize: Int - get() = ApplicationBase.instance.store().get(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_TEXT_SIZE, value) + get() = sAppPreferences.get(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) + set(value) = sAppPreferences.put(STORE_KEY_TEXT_SIZE, value) class FontSizeBottomSheet : LithoBottomSheet() { @@ -39,7 +41,7 @@ class FontSizeBottomSheet : LithoBottomSheet() { .textSizeDip(sEditorTextSize.toFloat()) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.note_option_font_size_example) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(CounterChooser.create(componentContext) .value(sEditorTextSize) .minValue(TEXT_SIZE_MIN) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt index 9ca243db..376eafbd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt @@ -4,20 +4,20 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem import com.maubis.scarlet.base.support.utils.* const val KEY_INTERNAL_ENABLE_FULL_SCREEN = "internal_enable_full_screen" var sInternalEnableFullScreen: Boolean - get() = ApplicationBase.instance.store().get(KEY_INTERNAL_ENABLE_FULL_SCREEN, false) - set(value) = ApplicationBase.instance.store().put(KEY_INTERNAL_ENABLE_FULL_SCREEN, value) + get() = sAppPreferences.get(KEY_INTERNAL_ENABLE_FULL_SCREEN, false) + set(value) = sAppPreferences.put(KEY_INTERNAL_ENABLE_FULL_SCREEN, value) const val KEY_INTERNAL_SHOW_UUID = "internal_show_uuid" var sInternalShowUUID: Boolean - get() = ApplicationBase.instance.store().get(KEY_INTERNAL_SHOW_UUID, false) - set(value) = ApplicationBase.instance.store().put(KEY_INTERNAL_SHOW_UUID, value) + get() = sAppPreferences.get(KEY_INTERNAL_SHOW_UUID, false) + set(value) = sAppPreferences.put(KEY_INTERNAL_SHOW_UUID, value) class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.internal_settings_title diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt index e647f541..ac517b0b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt @@ -7,7 +7,7 @@ import com.facebook.litho.ComponentContext import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -22,8 +22,8 @@ const val LINE_COUNT_MIN = 2 const val LINE_COUNT_MAX = 15 var sNoteItemLineCount: Int - get() = ApplicationBase.instance.store().get(STORE_KEY_LINE_COUNT, LINE_COUNT_DEFAULT) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_LINE_COUNT, value) + get() = sAppPreferences.get(STORE_KEY_LINE_COUNT, LINE_COUNT_DEFAULT) + set(value) = sAppPreferences.put(STORE_KEY_LINE_COUNT, value) class LineCountBottomSheet : LithoBottomSheet() { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 1d219db5..2480661a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -12,6 +12,7 @@ import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -37,18 +38,18 @@ class OpenSourceBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(openSourceDetails) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.osp_page_libraries) .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child(Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 4f) .text(Markdown.render(LIBRARY_DETAILS_MD, true)) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(BottomSheetBar.create(componentContext) .primaryActionRes(R.string.about_page_contribute) .onPrimaryClick { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index 1f05c315..6446c8ed 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.sheets.openCreateSheet @@ -21,17 +21,17 @@ const val KEY_APP_LOCK_ENABLED = "app_lock_enabled" const val KEY_ASK_PIN_ALWAYS = "ask_pin_always" var sSecurityCode: String - get() = ApplicationBase.instance.store().get(KEY_SECURITY_CODE, "") - set(value) = ApplicationBase.instance.store().put(KEY_SECURITY_CODE, value) + get() = sAppPreferences.get(KEY_SECURITY_CODE, "") + set(value) = sAppPreferences.put(KEY_SECURITY_CODE, value) var sSecurityFingerprintEnabled: Boolean - get() = ApplicationBase.instance.store().get(KEY_FINGERPRINT_ENABLED, true) - set(value) = ApplicationBase.instance.store().put(KEY_FINGERPRINT_ENABLED, value) + get() = sAppPreferences.get(KEY_FINGERPRINT_ENABLED, true) + set(value) = sAppPreferences.put(KEY_FINGERPRINT_ENABLED, value) var sSecurityAppLockEnabled: Boolean - get() = ApplicationBase.instance.store().get(KEY_APP_LOCK_ENABLED, false) - set(value) = ApplicationBase.instance.store().put(KEY_APP_LOCK_ENABLED, value) + get() = sAppPreferences.get(KEY_APP_LOCK_ENABLED, false) + set(value) = sAppPreferences.put(KEY_APP_LOCK_ENABLED, value) var sSecurityAskPinAlways: Boolean - get() = ApplicationBase.instance.store().get(KEY_ASK_PIN_ALWAYS, true) - set(value) = ApplicationBase.instance.store().put(KEY_ASK_PIN_ALWAYS, value) + get() = sAppPreferences.get(KEY_ASK_PIN_ALWAYS, true) + set(value) = sAppPreferences.put(KEY_ASK_PIN_ALWAYS, value) class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.security_option_title diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt index 0b9c4d32..77069bf0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt @@ -4,7 +4,7 @@ import android.app.Dialog import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.core.note.SortingTechnique import com.maubis.scarlet.base.support.sheets.LithoChooseOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoChooseOptionsItem @@ -37,7 +37,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { const val KEY_SORTING_TECHNIQUE = "KEY_SORTING_TECHNIQUE" fun getSortingState(): SortingTechnique { - return SortingTechnique.values()[ApplicationBase.instance.store().get(KEY_SORTING_TECHNIQUE, SortingTechnique.LAST_MODIFIED.ordinal)] + return SortingTechnique.values()[sAppPreferences.get(KEY_SORTING_TECHNIQUE, SortingTechnique.LAST_MODIFIED.ordinal)] } fun getSortingTechniqueLabel(technique: SortingTechnique): Int { @@ -52,7 +52,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { } fun setSortingState(sortingTechnique: SortingTechnique) { - ApplicationBase.instance.store().put(KEY_SORTING_TECHNIQUE, sortingTechnique.ordinal) + sAppPreferences.put(KEY_SORTING_TECHNIQUE, sortingTechnique.ordinal) } fun openSheet(activity: MainActivity, listener: () -> Unit) { diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index 14f00fb0..80f2f8d6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -12,6 +12,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.support.sheets.* import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -104,7 +105,7 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { sAutomaticTheme = !sAutomaticTheme if (sAutomaticTheme) { setThemeFromSystem(context) - onThemeChange(ApplicationBase.sAppTheme.get()) + onThemeChange(sAppTheme.get()) } reset(context, dialog) }) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index d552c905..6d273771 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -6,6 +6,8 @@ import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.performAction import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet.Companion.getSortingState @@ -24,7 +26,7 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { options.add(LithoOptionsItem( title = R.string.home_option_theme_color, subtitle = R.string.home_option_theme_color_subtitle, - icon = if (ApplicationBase.sAppTheme.isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, + icon = if (sAppTheme.isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, listener = { activity.performAction(MainActivityActions.COLOR_PICKER) } @@ -128,15 +130,15 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { } var useGridView: Boolean - get() = ApplicationBase.instance.store().get(KEY_LIST_VIEW, true) - set(isGrid) = ApplicationBase.instance.store().put(KEY_LIST_VIEW, isGrid) + get() = sAppPreferences.get(KEY_LIST_VIEW, true) + set(isGrid) = sAppPreferences.put(KEY_LIST_VIEW, isGrid) var useNoteColorAsBackground: Boolean - get() = ApplicationBase.instance.store().get(KEY_NOTE_VIEWER_BG_COLOR, false) - set(value) = ApplicationBase.instance.store().put(KEY_NOTE_VIEWER_BG_COLOR, value) + get() = sAppPreferences.get(KEY_NOTE_VIEWER_BG_COLOR, false) + set(value) = sAppPreferences.put(KEY_NOTE_VIEWER_BG_COLOR, value) var sMarkdownEnabledHome: Boolean - get() = ApplicationBase.instance.store().get(KEY_MARKDOWN_HOME_ENABLED, true) - set(value) = ApplicationBase.instance.store().put(KEY_MARKDOWN_HOME_ENABLED, value) + get() = sAppPreferences.get(KEY_MARKDOWN_HOME_ENABLED, true) + set(value) = sAppPreferences.put(KEY_MARKDOWN_HOME_ENABLED, value) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 78da6759..842a5b2e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -3,6 +3,8 @@ package com.maubis.scarlet.base.support.database import android.content.Context import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteMeta import com.maubis.scarlet.base.core.note.Reminder @@ -30,9 +32,9 @@ class Migrator(val context: Context) { fun start() { runTask(KEY_MIGRATE_THEME) { - val isNightMode = ApplicationBase.instance.store().get(KEY_NIGHT_THEME, true) + val isNightMode = sAppPreferences.get(KEY_NIGHT_THEME, true) sAppThemeLabel = if (isNightMode) Theme.DARK.name else Theme.LIGHT.name - ApplicationBase.sAppTheme.notifyChange(context) + sAppTheme.notifyChange(context) } runTask(key = KEY_MIGRATE_REMINDERS) { val notes = notesDb.getAll() @@ -64,7 +66,7 @@ class Migrator(val context: Context) { getLastUsedAppVersionCode() == 0, KEY_MIGRATE_DEFAULT_VALUES) { sAppThemeLabel = Theme.DARK.name - ApplicationBase.instance.store().put(KEY_LIST_VIEW, true) + sAppPreferences.put(KEY_LIST_VIEW, true) } runTask(KEY_MIGRATE_TO_GDRIVE_DATABASE) { @@ -84,7 +86,7 @@ class Migrator(val context: Context) { } private fun runTask(key: String, task: () -> Unit) { - if (ApplicationBase.instance.store().get(key, false)) { + if (sAppPreferences.get(key, false)) { return } @@ -93,7 +95,7 @@ class Migrator(val context: Context) { } catch (exception: Exception) { maybeThrow(exception) } - ApplicationBase.instance.store().put(key, true) + sAppPreferences.put(key, true) } private fun runTaskIf(condition: Boolean, key: String, task: () -> Unit) { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt index d46d6942..7567cc28 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt @@ -7,6 +7,7 @@ import android.widget.TextView import com.github.bijoysingh.uibasics.views.UILabelView import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -36,7 +37,7 @@ abstract class GridBottomSheetBase : ThemedBottomSheetFragment() { fun setOptionTitle(dialog: Dialog, title: Int) { GlobalScope.launch(Dispatchers.Main) { val titleView = dialog.findViewById(R.id.options_title) - titleView.setTextColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + titleView.setTextColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) titleView.setText(title) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 619124bb..05cf6cdf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -19,6 +19,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.ui.BottomSheetTabletDialog @@ -36,7 +37,7 @@ fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { .marginDip(YogaEdge.TOP, 18f) .marginDip(YogaEdge.BOTTOM, 8f) .textStyle(Typeface.BOLD) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) } fun getLithoBottomSheetButton(context: ComponentContext): Text.Builder { @@ -82,7 +83,7 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { } fun getFullComponent(componentContext: ComponentContext, dialog: Dialog, childComponent: Component) { - val topHandle = when (ApplicationBase.sAppTheme.isNightTheme()) { + val topHandle = when (sAppTheme.isNightTheme()) { true -> R.drawable.bottom_sheet_top_handle_dark false -> R.drawable.bottom_sheet_top_handle_light } @@ -118,7 +119,7 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { abstract fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component - open fun backgroundColor(componentContext: ComponentContext) = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) + open fun backgroundColor(componentContext: ComponentContext) = sAppTheme.get(ThemeColorType.BACKGROUND) open fun topMargin() = 16f diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt index 1d328d2e..78276b1c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -27,8 +28,7 @@ object ChooseOptionItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoChooseOptionsItem): Component { - val theme = ApplicationBase.sAppTheme - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) val selectedColor = context.getColor(R.color.colorAccent) val row = Row.create(context) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt index c1bcab46..ab72af34 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_OPEN_SANS import com.maubis.scarlet.base.support.specs.RoundIcon @@ -34,10 +35,9 @@ object OptionItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoOptionsItem): Component { - val theme = ApplicationBase.sAppTheme - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) - val subtitleColor = theme.get(ThemeColorType.HINT_TEXT) - val selectedColor = theme.get(ThemeColorType.ACCENT_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val subtitleColor = sAppTheme.get(ThemeColorType.HINT_TEXT) + val selectedColor = sAppTheme.get(ThemeColorType.ACCENT_TEXT) val subtitle = when (option.subtitle) { 0 -> option.content @@ -123,8 +123,7 @@ object OptionLabelItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: LithoLabelOptionsItem): Component { - val theme = ApplicationBase.sAppTheme - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) val row = Column.create(context) .widthPercent(100f) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt index 3d0d578c..d22754bb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt @@ -10,6 +10,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetButton import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -36,7 +37,7 @@ object BottomSheetBarSpec { .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .clickHandler(BottomSheetBar.onSecondaryClickEvent(context))) } row.child(EmptySpec.create(context).flexGrow(1f)) @@ -48,7 +49,7 @@ object BottomSheetBarSpec { .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .clickHandler(BottomSheetBar.onTertiaryClickEvent(context))) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt index 0143415a..c004c1e4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt @@ -11,6 +11,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -34,7 +35,7 @@ object CounterChooserSpec { .typeface(CoreConfig.FONT_MONSERRAT) .textSizeRes(R.dimen.font_size_xxxlarge) .paddingDip(YogaEdge.HORIZONTAL, 12f) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(bottomBarRoundIcon(context, ToolbarColorConfig()) .iconRes(R.drawable.icon_more_counter) .onClick { onValueChange(Math.min(value + 1, maxValue)) }) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt index 29f876f8..33aee1f6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -11,6 +11,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -86,7 +87,7 @@ object GridSectionViewSpec { @Prop(optional = true) maxLines: Int?, @Prop(optional = true) showSeparator: Boolean?): Component { val column = Column.create(context) - val primaryColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val primaryColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) if (section.title != 0) { column.child( @@ -138,7 +139,7 @@ object GridSectionViewSpec { if (showSeparator == true) { column.child(SolidColor.create(context) - .color(ApplicationBase.sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .color(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .heightDip(1.5f) .widthDip(196f) .alignSelf(YogaAlign.CENTER) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt index 4feca05f..7ae3148d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.ui.ThemeColorType object EmptySpec { @@ -41,8 +42,8 @@ fun separatorSpec(context: ComponentContext): Component.Builder<*> { data class ToolbarColorConfig( - var toolbarBackgroundColor: Int = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), - var toolbarIconColor: Int = ApplicationBase.sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) + var toolbarBackgroundColor: Int = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), + var toolbarIconColor: Int = sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) fun bottomBarRoundIcon(context: ComponentContext, colorConfig: ToolbarColorConfig): RoundIcon.Builder { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt index 7d8fa248..9e9c430f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt @@ -3,6 +3,7 @@ package com.maubis.scarlet.base.support.ui import android.graphics.* import android.graphics.drawable.Drawable import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme class CircleDrawable(color: Int, showBorder: Boolean = true) : Drawable() { private val paint: Paint @@ -13,7 +14,7 @@ class CircleDrawable(color: Int, showBorder: Boolean = true) : Drawable() { this.paint = Paint(Paint.ANTI_ALIAS_FLAG) this.paint.color = color - val isNightTheme = ApplicationBase.sAppTheme.isNightTheme() + val isNightTheme = sAppTheme.isNightTheme() this.borderPaint = Paint(Paint.ANTI_ALIAS_FLAG) this.borderPaint.color = when { !showBorder -> Color.TRANSPARENT diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt index cabc317f..73bef78d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt @@ -3,6 +3,7 @@ package com.maubis.scarlet.base.support.ui import android.graphics.* import com.facebook.litho.drawable.ComparableDrawable import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean = false) : ComparableDrawable() { private val mPaint: Paint @@ -14,7 +15,7 @@ class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean this.mPaint.color = color this.mPaint.alpha = alpha - val isNightTheme = ApplicationBase.sAppTheme.isNightTheme() + val isNightTheme = sAppTheme.isNightTheme() this.mBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG) this.mBorderPaint.color = when { !showBorder -> Color.TRANSPARENT diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index a074e047..1293a609 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -7,20 +7,20 @@ import android.support.v4.content.ContextCompat import com.github.bijoysingh.starter.util.DimensionManager import com.maubis.markdown.MarkdownConfig import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.throwOrReturn import java.lang.ref.WeakReference const val KEY_APP_THEME = "KEY_APP_THEME" var sAppThemeLabel: String - get() = ApplicationBase.instance.store().get(KEY_APP_THEME, Theme.DARK.name) - set(value) = ApplicationBase.instance.store().put(KEY_APP_THEME, value) + get() = sAppPreferences.get(KEY_APP_THEME, Theme.DARK.name) + set(value) = sAppPreferences.put(KEY_APP_THEME, value) const val KEY_AUTOMATIC_THEME = "automatic_theme" var sAutomaticTheme: Boolean - get() = ApplicationBase.instance.store().get(KEY_AUTOMATIC_THEME, false) - set(value) = ApplicationBase.instance.store().put(KEY_AUTOMATIC_THEME, value) + get() = sAppPreferences.get(KEY_AUTOMATIC_THEME, false) + set(value) = sAppPreferences.put(KEY_AUTOMATIC_THEME, value) fun setThemeFromSystem(context: Context) { val configuration = context.resources.configuration diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index 6510a237..aec0a50b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -7,6 +7,7 @@ import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.settings.sheet.sInternalEnableFullScreen import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.maybeThrow @@ -21,7 +22,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - ApplicationBase.sAppTheme.register(this) + sAppTheme.register(this) } fun setSystemTheme(color: Int = getStatusBarColor()) { @@ -40,7 +41,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { return } setThemeFromSystem(this) - ApplicationBase.sAppTheme.notifyChange(this) + sAppTheme.notifyChange(this) } fun fullScreenView() { @@ -68,7 +69,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { if (OsVersionUtils.canSetStatusBarTheme()) { val view = window.decorView var flags = view.systemUiVisibility - flags = when (ApplicationBase.sAppTheme.isNightTheme()) { + flags = when (sAppTheme.isNightTheme()) { true -> flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() false -> flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } @@ -76,9 +77,9 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { } } - fun getThemeColor(): Int = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) + fun getThemeColor(): Int = sAppTheme.get(ThemeColorType.BACKGROUND) - fun getStatusBarColor(): Int = ApplicationBase.sAppTheme.get(ThemeColorType.STATUS_BAR) + fun getStatusBarColor(): Int = sAppTheme.get(ThemeColorType.STATUS_BAR) fun tryClosingTheKeyboard() { try { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt index 83f87914..98e817c0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt @@ -13,6 +13,7 @@ import com.github.bijoysingh.starter.fragments.SimpleBottomSheetFragment import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { @@ -45,7 +46,7 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { abstract fun getBackgroundView(): Int fun resetBackground(dialog: Dialog) { - val backgroundColor = ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND) + val backgroundColor = sAppTheme.get(ThemeColorType.BACKGROUND) val containerLayout = dialog.findViewById(getBackgroundView()) containerLayout.setBackgroundColor(backgroundColor) for (viewId in getBackgroundCardViewIds()) { @@ -56,8 +57,8 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { open fun getOptionsTitleColor(selected: Boolean): Int { val colorResource = when { - ApplicationBase.sAppTheme.isNightTheme() && selected -> R.color.material_blue_300 - ApplicationBase.sAppTheme.isNightTheme() -> R.color.light_secondary_text + sAppTheme.isNightTheme() && selected -> R.color.material_blue_300 + sAppTheme.isNightTheme() -> R.color.light_secondary_text selected -> R.color.material_blue_700 else -> R.color.dark_secondary_text } @@ -66,8 +67,8 @@ abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { open fun getOptionsSubtitleColor(selected: Boolean): Int { val colorResource = when { - ApplicationBase.sAppTheme.isNightTheme() && selected -> R.color.material_blue_200 - ApplicationBase.sAppTheme.isNightTheme() -> R.color.light_tertiary_text + sAppTheme.isNightTheme() && selected -> R.color.material_blue_200 + sAppTheme.isNightTheme() -> R.color.light_tertiary_text selected -> R.color.material_blue_500 else -> R.color.dark_tertiary_text } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt index ee51800b..efac3363 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/AppVersionUtils.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.support.utils import com.maubis.scarlet.base.BuildConfig -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.main.sheets.WHATS_NEW_SHEET_INDEX import java.util.* @@ -20,7 +20,7 @@ fun getCurrentVersionCode(): Int { * If nothing can be concluded it's 0 (assumes new user) */ fun getLastUsedAppVersionCode(): Int { - val appVersion = ApplicationBase.instance.store().get(KEY_LAST_KNOWN_APP_VERSION, 0) + val appVersion = sAppPreferences.get(KEY_LAST_KNOWN_APP_VERSION, 0) return when { appVersion > 0 -> appVersion notesDb.getCount() > 0 -> -1 @@ -29,7 +29,7 @@ fun getLastUsedAppVersionCode(): Int { } fun shouldShowWhatsNewSheet(): Boolean { - val lastShownWhatsNew = ApplicationBase.instance.store().get(KEY_LAST_SHOWN_WHATS_NEW, 0) + val lastShownWhatsNew = sAppPreferences.get(KEY_LAST_SHOWN_WHATS_NEW, 0) if (lastShownWhatsNew >= WHATS_NEW_SHEET_INDEX) { // Already shown the latest return false @@ -38,18 +38,18 @@ fun shouldShowWhatsNewSheet(): Boolean { val lastUsedAppVersion = getLastUsedAppVersionCode() // Update the values independent of the decision - ApplicationBase.instance.store().put(KEY_LAST_SHOWN_WHATS_NEW, WHATS_NEW_SHEET_INDEX) - ApplicationBase.instance.store().put(KEY_LAST_KNOWN_APP_VERSION, getCurrentVersionCode()) + sAppPreferences.put(KEY_LAST_SHOWN_WHATS_NEW, WHATS_NEW_SHEET_INDEX) + sAppPreferences.put(KEY_LAST_KNOWN_APP_VERSION, getCurrentVersionCode()) // New users don't need to see the whats new screen return lastUsedAppVersion != 0 } fun getInstanceID(): String { - val deviceId = ApplicationBase.instance.store().get(KEY_INSTANCE_ID, "") + val deviceId = sAppPreferences.get(KEY_INSTANCE_ID, "") if (deviceId.isBlank()) { val newDeviceId = UUID.randomUUID().toString() - ApplicationBase.instance.store().put(KEY_INSTANCE_ID, newDeviceId) + sAppPreferences.put(KEY_INSTANCE_ID, newDeviceId) return newDeviceId } return deviceId diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt index c2ea9485..7e185d7b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt @@ -6,6 +6,7 @@ import android.util.Log import com.github.bijoysingh.starter.util.DateFormatter import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType @@ -20,23 +21,23 @@ import kotlinx.coroutines.launch const val KEY_INTERNAL_LOG_TRACES_TO_NOTE = "internal_log_traces_to_note" var sInternalLogTracesToNote: Boolean - get() = instance.store().get(KEY_INTERNAL_LOG_TRACES_TO_NOTE, false) - set(value) = instance.store().put(KEY_INTERNAL_LOG_TRACES_TO_NOTE, value) + get() = sAppPreferences.get(KEY_INTERNAL_LOG_TRACES_TO_NOTE, false) + set(value) = sAppPreferences.put(KEY_INTERNAL_LOG_TRACES_TO_NOTE, value) const val KEY_INTERNAL_SHOW_TRACES_IN_SHEET = "internal_show_traces_in_sheet" var sInternalShowTracesInSheet: Boolean - get() = instance.store().get(KEY_INTERNAL_SHOW_TRACES_IN_SHEET, false) - set(value) = instance.store().put(KEY_INTERNAL_SHOW_TRACES_IN_SHEET, value) + get() = sAppPreferences.get(KEY_INTERNAL_SHOW_TRACES_IN_SHEET, false) + set(value) = sAppPreferences.put(KEY_INTERNAL_SHOW_TRACES_IN_SHEET, value) const val KEY_INTERNAL_THROW_ON_EXCEPTION = "internal_throw_on_exception" var sInternalThrowOnException: Boolean - get() = instance.store().get(KEY_INTERNAL_THROW_ON_EXCEPTION, false) - set(value) = instance.store().put(KEY_INTERNAL_THROW_ON_EXCEPTION, value) + get() = sAppPreferences.get(KEY_INTERNAL_THROW_ON_EXCEPTION, false) + set(value) = sAppPreferences.put(KEY_INTERNAL_THROW_ON_EXCEPTION, value) const val KEY_INTERNAL_THROWN_EXCEPTION_COUNT = "internal_thrown_exception_count" var sInternalThrownExceptionCount: Int - get() = instance.store().get(KEY_INTERNAL_THROWN_EXCEPTION_COUNT, 0) - set(value) = instance.store().put(KEY_INTERNAL_THROWN_EXCEPTION_COUNT, value) + get() = sAppPreferences.get(KEY_INTERNAL_THROWN_EXCEPTION_COUNT, 0) + set(value) = sAppPreferences.put(KEY_INTERNAL_THROWN_EXCEPTION_COUNT, value) /** * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt index f9e6b472..a43ef511 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt @@ -1,8 +1,8 @@ package com.maubis.scarlet.base.support.utils import android.content.Context -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -26,9 +26,9 @@ object FlavorUtils { } catch (e: Exception) { throwOrReturn(e, false) } - ApplicationBase.instance.store().put(KEY_PRO_APP_INSTALLED, found) + sAppPreferences.put(KEY_PRO_APP_INSTALLED, found) } - return ApplicationBase.instance.store().get(KEY_PRO_APP_INSTALLED, false) + return sAppPreferences.get(KEY_PRO_APP_INSTALLED, false) } fun isPro() = instance.appFlavor() == Flavor.PRO diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index fa591c42..7cc3f70a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -10,7 +10,7 @@ import com.facebook.litho.ComponentContext import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.sort @@ -32,33 +32,33 @@ import kotlinx.coroutines.launch const val STORE_KEY_WIDGET_ENABLE_FORMATTING = "widget_enable_formatting" var sWidgetEnableFormatting: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_ENABLE_FORMATTING, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_ENABLE_FORMATTING, value) + get() = sAppPreferences.get(STORE_KEY_WIDGET_ENABLE_FORMATTING, true) + set(value) = sAppPreferences.put(STORE_KEY_WIDGET_ENABLE_FORMATTING, value) const val STORE_KEY_WIDGET_SHOW_LOCKED_NOTES = "widget_show_locked_notes" var sWidgetShowLockedNotes: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, false) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, value) + get() = sAppPreferences.get(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, false) + set(value) = sAppPreferences.put(STORE_KEY_WIDGET_SHOW_LOCKED_NOTES, value) const val STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES = "widget_show_archived_notes" var sWidgetShowArchivedNotes: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, value) + get() = sAppPreferences.get(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, true) + set(value) = sAppPreferences.put(STORE_KEY_WIDGET_SHOW_ARCHIVED_NOTES, value) const val STORE_KEY_WIDGET_SHOW_TRASH_NOTES = "widget_show_trash_notes" var sWidgetShowDeletedNotes: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, false) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, value) + get() = sAppPreferences.get(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, false) + set(value) = sAppPreferences.put(STORE_KEY_WIDGET_SHOW_TRASH_NOTES, value) const val STORE_KEY_WIDGET_BACKGROUND_COLOR = "widget_background_color_v1" var sWidgetBackgroundColor: Int - get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_BACKGROUND_COLOR, 0x65000000) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_BACKGROUND_COLOR, value) + get() = sAppPreferences.get(STORE_KEY_WIDGET_BACKGROUND_COLOR, 0x65000000) + set(value) = sAppPreferences.put(STORE_KEY_WIDGET_BACKGROUND_COLOR, value) const val STORE_KEY_WIDGET_SHOW_TOOLBAR = "widget_show_toolbar" var sWidgetShowToolbar: Boolean - get() = ApplicationBase.instance.store().get(STORE_KEY_WIDGET_SHOW_TOOLBAR, true) - set(value) = ApplicationBase.instance.store().put(STORE_KEY_WIDGET_SHOW_TOOLBAR, value) + get() = sAppPreferences.get(STORE_KEY_WIDGET_SHOW_TOOLBAR, true) + set(value) = sAppPreferences.put(STORE_KEY_WIDGET_SHOW_TOOLBAR, value) fun getWidgetNoteText(note: Note): CharSequence { if (note.locked && !sWidgetShowLockedNotes) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt index 25a95767..f86c45d9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt @@ -9,7 +9,7 @@ import com.bijoysingh.quicknote.drive.toImageUUID import com.bijoysingh.quicknote.firebase.data.getFirebaseNote import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.note.NoteBuilder @@ -374,7 +374,7 @@ abstract class RemoteController(private val RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(data.uuid) when { - imageUUID !== null -> noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid).apply { + imageUUID !== null -> sAppImageStorage.getFile(imageUUID.noteUuid, imageUUID.imageUuid).apply { imageSync.insert(data, this) } else -> null @@ -474,7 +474,7 @@ abstract class RemoteController(private val RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(data.uuid) if (imageUUID !== null) { - val imageFile = ApplicationBase.noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + val imageFile = ApplicationBase.sAppImageStorage.getFile(imageUUID.noteUuid, imageUUID.imageUuid) if (imageFile.exists()) { remoteDatabaseController.remoteDatabaseUpdate(RemoteDataType.IMAGE, data.uuid, databaseUpdateLambda) return @@ -514,7 +514,7 @@ abstract class RemoteController(private val RemoteDataType.IMAGE -> { val imageUUID = toImageUUID(data.uuid) if (imageUUID !== null) { - val imageFile = ApplicationBase.noteImagesFolder.getFile(imageUUID.noteUuid, imageUUID.imageUuid) + val imageFile = ApplicationBase.sAppImageStorage.getFile(imageUUID.noteUuid, imageUUID.imageUuid) imageFile.delete() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index 7f399e86..8a131172 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.LithoCircleDrawable @@ -28,7 +29,7 @@ object GDriveRootViewSpec { else -> R.string.google_drive_page_login_button } return Column.create(context) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -38,7 +39,7 @@ object GDriveRootViewSpec { .marginDip(YogaEdge.HORIZONTAL, 16f) .paddingDip(YogaEdge.VERTICAL, 8f) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.google_drive_page_login_firebase_button) .textAlignment(Layout.Alignment.ALIGN_CENTER) .typeface(CoreConfig.FONT_MONSERRAT) @@ -83,15 +84,15 @@ object GDriveContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_login_title) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_login_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(GDriveIconView.create(context) @@ -118,7 +119,7 @@ object GDriveIconViewSpec { .paddingDip(YogaEdge.HORIZONTAL, 32f) .paddingDip(YogaEdge.VERTICAL, 24f) .child(Image.create(context) - .drawable(icon.color(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) + .drawable(icon.color(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) .paddingDip(YogaEdge.ALL, 12f) .marginDip(YogaEdge.BOTTOM, 12f) @@ -127,7 +128,7 @@ object GDriveIconViewSpec { .text(title) .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT)) .build() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt index 1409e441..81d2277a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -26,7 +27,7 @@ object GDriveLogoutRootViewSpec { else -> R.string.google_drive_page_logout_button } return Column.create(context) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -65,15 +66,15 @@ object GDriveLogoutContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_logout_title) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_logout_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(GDriveIconView.create(context) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index 1f1ab206..14aa0400 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -17,6 +17,7 @@ import com.google.gson.Gson import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.export.data.getExportableNoteMeta import com.maubis.scarlet.base.note.getFullText @@ -41,7 +42,7 @@ object PendingItemIconSpec { fun onCreate(context: ComponentContext, @Prop(resType = ResType.STRING) label: String, @Prop(resType = ResType.DRAWABLE) icon: Drawable): Component { - val secondaryColor = ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val secondaryColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) return Row.create(context) .paddingDip(YogaEdge.HORIZONTAL, 8f) .paddingDip(YogaEdge.VERTICAL, 4f) @@ -66,10 +67,9 @@ object PendingItemLayoutSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop option: PendingItem): Component { - val theme = ApplicationBase.sAppTheme - val titleColor = theme.get(ThemeColorType.SECONDARY_TEXT) - val subtitleColor = theme.get(ThemeColorType.TERTIARY_TEXT) - val hintColor = theme.get(ThemeColorType.HINT_TEXT) + val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + val subtitleColor = sAppTheme.get(ThemeColorType.TERTIARY_TEXT) + val hintColor = sAppTheme.get(ThemeColorType.HINT_TEXT) val uuid = option.state.uuid val info = option.info ?: "N/A" diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 93e8cdc1..10ea71c6 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -3,7 +3,7 @@ package com.bijoysingh.quicknote.drive import com.bijoysingh.quicknote.database.RemoteDataType import com.bijoysingh.quicknote.database.RemoteUploadData import com.bijoysingh.quicknote.database.RemoteUploadDataDao -import com.maubis.scarlet.base.config.ApplicationBase.Companion.noteImagesFolder +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -138,7 +138,7 @@ class GDriveRemoteImageFolder( val gDriveUUID = id.name() val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp ?: getTrueCurrentTime() - val imageFile = noteImagesFolder.getFile(id.noteUuid, id.imageUuid) + val imageFile = sAppImageStorage.getFile(id.noteUuid, id.imageUuid) if (!imageFile.exists()) { // notifyDriveData(id, gDriveUUID, timestamp) val existing = database.getByUUID(RemoteDataType.IMAGE.name, gDriveUUID) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt index 4f56e47a..3c52ff5b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/DataPolicyActivity.kt @@ -12,6 +12,7 @@ import com.bijoysingh.quicknote.R import com.github.bijoysingh.starter.util.IntentUtils import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.settings.sheet.PRIVACY_POLICY_LINK import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.visibility @@ -86,9 +87,9 @@ class DataPolicyActivity : ThemedActivity() { const val DATA_POLICY_VERSION = 4 const val DATA_POLICY_ACCEPTED = "DATA_POLICY_ACCEPTED_VERSION" - fun hasAcceptedThePolicy() = ApplicationBase.instance.store().get(DATA_POLICY_ACCEPTED, 0) == DATA_POLICY_VERSION + fun hasAcceptedThePolicy() = sAppPreferences.get(DATA_POLICY_ACCEPTED, 0) == DATA_POLICY_VERSION - fun acceptThePolicy() = ApplicationBase.instance.store().put(DATA_POLICY_ACCEPTED, DATA_POLICY_VERSION) + fun acceptThePolicy() = sAppPreferences.put(DATA_POLICY_ACCEPTED, DATA_POLICY_VERSION) fun openIfNeeded(activity: AppCompatActivity) { if (!hasAcceptedThePolicy() && ApplicationBase.instance.authenticator().isLegacyLoggedIn()) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt index f94d4ce3..ed182f23 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt @@ -12,6 +12,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.LithoCircleDrawable @@ -27,7 +28,7 @@ object FirebaseRootViewSpec { else -> R.string.firebase_page_login_button } return Column.create(context) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -66,15 +67,15 @@ object FirebaseContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.firebase_page_login_title) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_page_important_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(FirebaseIconView.create(context) @@ -101,7 +102,7 @@ object FirebaseIconViewSpec { .paddingDip(YogaEdge.HORIZONTAL, 32f) .paddingDip(YogaEdge.VERTICAL, 24f) .child(Image.create(context) - .drawable(icon.color(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) + .drawable(icon.color(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) .paddingDip(YogaEdge.ALL, 12f) .marginDip(YogaEdge.BOTTOM, 12f) @@ -110,7 +111,7 @@ object FirebaseIconViewSpec { .text(title) .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT)) .build() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt index e7144630..688dcd08 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -26,7 +26,7 @@ import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.maybeThrow @@ -138,7 +138,7 @@ class FirebaseLoginActivity : ThemedActivity() { return } - ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) + sAppPreferences.put(KEY_FORCE_SHOW_SIGN_IN, true) setButton(false) initFirebaseDatabase(context, user.uid) finish() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt index 44f42cde..7ad1551a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt @@ -13,6 +13,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -26,7 +27,7 @@ object FirebaseRemovalRootViewSpec { else -> R.string.firebase_removal_page_clear_button } return Column.create(context) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(VerticalScroll.create(context) .flexGrow(1f) .marginDip(YogaEdge.ALL, 8f) @@ -65,15 +66,15 @@ object FirebaseRemovalContentViewSpec { fun onCreate(context: ComponentContext): Component { return Column.create(context) .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(ApplicationBase.sAppTheme.get(ThemeColorType.BACKGROUND)) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.firebase_removal_page_login_title) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_removal_page_important_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(Row.create(context) @@ -95,7 +96,7 @@ object FirebaseRemovalContentViewSpec { .paddingDip(YogaEdge.BOTTOM, 10f) .paddingDip(YogaEdge.TOP, 20f) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_whats_next_details) .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) .child(Text.create(context) @@ -104,7 +105,7 @@ object FirebaseRemovalContentViewSpec { .marginDip(YogaEdge.HORIZONTAL, 16f) .paddingDip(YogaEdge.ALL, 12f) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_remove_details) .typeface(CoreConfig.FONT_MONSERRAT)) .child(Text.create(context) @@ -113,7 +114,7 @@ object FirebaseRemovalContentViewSpec { .marginDip(YogaEdge.HORIZONTAL, 16f) .paddingDip(YogaEdge.ALL, 12f) .textSizeRes(R.dimen.font_size_normal) - .textColor(ApplicationBase.sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_next_details) .typeface(CoreConfig.FONT_MONSERRAT)) .build() diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt index 3e6d3f6e..7b7feab1 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseAuthenticator.kt @@ -11,7 +11,7 @@ import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseNetworkException import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.FirebaseDatabase -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.main.recycler.KEY_FORCE_SHOW_SIGN_IN import com.maubis.scarlet.base.support.utils.maybeThrow import kotlinx.coroutines.GlobalScope @@ -76,7 +76,7 @@ class FirebaseAuthenticator() { } logout() - ApplicationBase.instance.store().put(KEY_FORCE_SHOW_SIGN_IN, true) + sAppPreferences.put(KEY_FORCE_SHOW_SIGN_IN, true) val handler = Handler(Looper.getMainLooper()) handler.post { ToastHelper.show(context, "You have been signed out of the app") diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt index 67430ac5..a98aedcb 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt @@ -9,6 +9,7 @@ import com.android.volley.toolbox.Volley import com.bijoysingh.quicknote.BuildConfig import com.google.gson.Gson import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.config.remote.RemoteConfig import com.maubis.scarlet.base.support.utils.Flavor @@ -22,7 +23,7 @@ const val KEY_RC_FULL_VERSION = "KEY_RC_FULL_VERSION" class RemoteConfigFetcher() : IRemoteConfigFetcher { override fun setup(context: Context) { - val lastFetched = ApplicationBase.instance.store().get(KEY_REMOTE_CONFIG_FETCH_TIME, 0L) + val lastFetched = sAppPreferences.get(KEY_REMOTE_CONFIG_FETCH_TIME, 0L) if (System.currentTimeMillis() > lastFetched + REMOTE_CONFIG_REFETCH_TIME_MS) { fetchConfig(context) } @@ -30,8 +31,8 @@ class RemoteConfigFetcher() : IRemoteConfigFetcher { override fun isLatestVersion(): Boolean { val latestVersion = when (ApplicationBase.instance.appFlavor()) { - Flavor.PRO -> ApplicationBase.instance.store().get(KEY_RC_FULL_VERSION, 0) - Flavor.LITE -> ApplicationBase.instance.store().get(KEY_RC_LITE_VERSION, 0) + Flavor.PRO -> sAppPreferences.get(KEY_RC_FULL_VERSION, 0) + Flavor.LITE -> sAppPreferences.get(KEY_RC_LITE_VERSION, 0) else -> 0 } return BuildConfig.VERSION_CODE >= latestVersion @@ -56,11 +57,11 @@ class RemoteConfigFetcher() : IRemoteConfigFetcher { return } - ApplicationBase.instance.store().put(KEY_REMOTE_CONFIG_FETCH_TIME, System.currentTimeMillis()) + sAppPreferences.put(KEY_REMOTE_CONFIG_FETCH_TIME, System.currentTimeMillis()) try { val config = Gson().fromJson(response, RemoteConfig::class.java) - ApplicationBase.instance.store().put(KEY_RC_LITE_VERSION, config.rc_lite_production_version ?: 0) - ApplicationBase.instance.store().put(KEY_RC_FULL_VERSION, config.rc_full_production_version ?: 0) + sAppPreferences.put(KEY_RC_LITE_VERSION, config.rc_lite_production_version ?: 0) + sAppPreferences.put(KEY_RC_FULL_VERSION, config.rc_full_production_version ?: 0) } catch (exception: Exception) { maybeThrow(exception) return From f49c2eae69b865b67dd3215bd3da0d8947c65447 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 31 Oct 2019 00:15:55 +0000 Subject: [PATCH 111/134] [Refactor] Removing unused imports and formatting code --- .../com/maubis/scarlet/base/MainActivity.kt | 202 +++++---- .../scarlet/base/MainActivityExtensions.kt | 4 +- .../maubis/scarlet/base/config/CoreConfig.kt | 9 +- .../scarlet/base/config/MaterialNoteConfig.kt | 4 +- .../base/config/remote/RemoteConfig.kt | 5 +- .../maubis/scarlet/base/core/format/Format.kt | 4 +- .../scarlet/base/core/format/FormatBuilder.kt | 3 +- .../base/core/note/MaterialNoteActor.kt | 16 +- .../scarlet/base/core/note/NoteExtensions.kt | 16 +- .../scarlet/base/core/note/NoteImage.kt | 7 +- .../scarlet/base/core/note/NoteReminder.kt | 14 +- .../scarlet/base/database/FoldersProvider.kt | 2 +- .../scarlet/base/database/TagsProvider.kt | 2 +- .../database/remote/IRemoteDatabaseUtils.kt | 10 +- .../base/database/room/AppDatabase.java | 5 +- .../base/database/room/note/NoteDao.java | 3 +- .../export/activity/ImportNoteActivity.kt | 16 +- .../base/export/data/ExportableData.kt | 86 ++-- .../base/export/data/ExportableExtensions.kt | 33 +- .../base/export/data/ExportableFileFormat.kt | 8 +- .../base/export/data/ExportableNote.kt | 90 ++-- .../export/recycler/FileImportViewHolder.kt | 5 +- .../base/export/recycler/FileRecyclerItem.kt | 9 +- .../export/remote/FolderRemoteDatabase.kt | 30 +- .../base/export/remote/RemoteFolder.kt | 11 +- .../base/export/remote/RemoteImagesFolder.kt | 1 - .../sheet/BackupSettingsOptionsBottomSheet.kt | 89 ++-- .../export/sheet/ExportNotesBottomSheet.kt | 129 +++--- .../sheet/ExternalFolderSyncBottomSheet.kt | 94 ++-- .../export/sheet/PermissionBottomSheet.kt | 45 +- .../base/export/support/ExternalFolderSync.kt | 33 +- .../base/export/support/NoteExporter.kt | 28 +- .../base/export/support/NoteImporter.kt | 19 +- .../base/export/support/PermissionUtils.kt | 3 +- .../activity/OpenTextIntentOrFileActivity.kt | 2 - .../main/activity/WidgetConfigureActivity.kt | 6 +- .../base/main/recycler/EmptyRecyclerHolder.kt | 4 +- .../main/recycler/InformationRecyclerItem.kt | 66 ++- .../recycler/ToolbarMainRecyclerHolder.kt | 1 - .../base/main/sheets/AlertBottomSheet.kt | 144 +++---- .../base/main/sheets/ExceptionBottomSheet.kt | 57 +-- .../main/sheets/HomeOptionsBottomSheet.kt | 249 ++++++----- .../sheets/InstallProUpsellBottomSheet.kt | 68 +-- .../base/main/sheets/WhatsNewBottomSheet.kt | 67 +-- .../main/specs/MainActivityBottomBarSpec.kt | 260 +++++------ .../scarlet/base/note/MarkdownExtensions.kt | 3 +- .../scarlet/base/note/NoteExtensions.kt | 42 +- .../note/actions/NoteOptionsBottomSheet.kt | 249 ++++++----- .../note/actions/TextToSpeechBottomSheet.kt | 2 - .../creation/activity/CreateNoteActivity.kt | 48 +-- .../activity/ShareToScarletRouterActivity.kt | 5 +- .../creation/activity/ViewNoteActivity.kt | 61 +-- .../sheet/EditorOptionsBottomSheet.kt | 120 +++--- .../creation/sheet/FormatActionBottomSheet.kt | 42 +- .../creation/sheet/MarkdownHelpBottomSheet.kt | 24 +- .../creation/specs/NoteViewBottomBarSpec.kt | 405 +++++++++--------- .../note/creation/specs/NoteViewTopBarSpec.kt | 8 +- .../base/note/folder/FolderOptionItem.kt | 12 +- .../base/note/folder/FolderRecyclerItem.kt | 14 +- .../note/folder/SelectorFolderRecyclerItem.kt | 1 - .../sheet/CreateOrEditFolderBottomSheet.kt | 15 +- .../folder/sheet/DeleteFolderBottomSheet.kt | 70 +-- .../sheet/FolderChooserBottomSheetBase.kt | 105 ++--- .../base/note/formats/FormatControllerList.kt | 158 +++---- .../formats/recycler/FormatListViewHolder.kt | 10 +- .../formats/recycler/FormatTextViewHolder.kt | 12 +- .../formats/recycler/FormatViewHolderBase.kt | 74 ++-- .../base/note/recycler/NoteAppAdapter.kt | 40 +- .../base/note/recycler/NoteRecyclerHolder.kt | 6 +- .../base/note/reminders/ReminderJob.kt | 15 +- .../reminders/sheet/ReminderBottomSheet.kt | 104 +++-- .../selection/activity/SelectNotesActivity.kt | 6 +- .../activity/SelectableNotesActivityBase.kt | 19 +- .../sheet/SelectedNotesOptionsBottomSheet.kt | 255 +++++------ .../scarlet/base/note/tag/TagOptionsItem.kt | 12 +- .../tag/sheet/CreateOrEditTagBottomSheet.kt | 14 +- .../sheet/SelectedTagChooserBottomSheet.kt | 46 +- .../note/tag/sheet/TagChooserBottomSheet.kt | 36 +- .../scarlet/base/note/tag/view/HomeTagView.kt | 1 - .../tag/view/TagsAndColorPickerViewHolder.kt | 54 +-- .../base/notification/NotificationHandler.kt | 87 ++-- .../base/security/activity/AppLockActivity.kt | 32 +- .../security/activity/AppLockActivitySpecs.kt | 165 +++---- .../security/sheets/NoPincodeBottomSheet.kt | 64 +-- .../security/sheets/PincodeBottomSheet.kt | 288 +++++++------ .../base/service/FloatingNoteService.kt | 38 +- .../service/SyncedNoteBroadcastReceiver.kt | 6 +- .../sheet/AboutSettingsOptionsBottomSheet.kt | 84 ++-- .../base/settings/sheet/AboutUsBottomSheet.kt | 93 ++-- .../settings/sheet/ColorPickerBottomSheet.kt | 97 +++-- .../sheet/DeleteAndMoreOptionsBottomSheet.kt | 103 ++--- .../settings/sheet/FontSizeBottomSheet.kt | 53 +-- .../InternalSettingsOptionsBottomSheet.kt | 43 +- .../settings/sheet/LineCountBottomSheet.kt | 43 +- .../settings/sheet/OpenSourceBottomSheet.kt | 137 +++--- .../sheet/SecurityOptionsBottomSheet.kt | 83 ++-- .../sheet/SettingsOptionsBottomSheet.kt | 120 +++--- .../sheet/SortingOptionsBottomSheet.kt | 5 +- .../sheet/ThemeColorPickerBottomSheet.kt | 147 ++++--- .../sheet/UISettingsOptionsBottomSheet.kt | 66 +-- .../scarlet/base/settings/view/ColorView.kt | 1 - .../scarlet/base/support/BitmapHelper.kt | 20 +- .../scarlet/base/support/SearchConfig.kt | 58 +-- .../scarlet/base/support/ShortcutUtils.kt | 5 +- .../base/support/database/HouseKeeper.kt | 24 +- .../base/support/database/MigrationUtils.kt | 4 +- .../base/support/option/OptionsItem.kt | 20 +- .../base/support/option/TagOptionsItemBase.kt | 12 +- .../support/recycler/SimpleItemTouchHelper.kt | 12 +- .../support/sheets/GridBottomSheetBase.kt | 1 - .../support/sheets/GridOptionBottomSheet.kt | 19 +- .../base/support/sheets/LithoBottomSheet.kt | 64 +-- .../sheets/LithoChooseOptionBottomSheet.kt | 79 ++-- .../support/sheets/LithoOptionBottomSheet.kt | 200 ++++----- .../base/support/specs/BottomSheetBarSpec.kt | 28 +- .../base/support/specs/CounterChooserSpec.kt | 42 +- .../base/support/specs/GridSectionViewSpec.kt | 159 +++---- .../base/support/specs/RoundIconSpec.kt | 63 +-- .../scarlet/base/support/specs/SpecUtils.kt | 61 ++- .../support/ui/BottomSheetTabletDialog.kt | 4 +- .../scarlet/base/support/ui/CircleDrawable.kt | 8 +- .../base/support/ui/LithoCircleDrawable.kt | 20 +- .../scarlet/base/support/ui/ThemeColorType.kt | 1 - .../scarlet/base/support/ui/ThemeManager.kt | 299 ++++++------- .../scarlet/base/support/ui/ThemedActivity.kt | 3 +- .../support/ui/ThemedBottomSheetFragment.kt | 1 - .../base/support/utils/DateFormatUtils.kt | 12 +- .../base/support/utils/ExceptionUtils.kt | 16 +- .../scarlet/base/support/utils/FlavorUtils.kt | 1 - .../scarlet/base/support/utils/ImageCache.kt | 1 - .../base/support/utils/TextInputUtils.kt | 4 +- .../base/widget/AllNotesWidgetProvider.kt | 6 +- .../base/widget/AllNotesWidgetService.kt | 3 +- .../base/widget/CreateNoteWidgetProvider.kt | 12 +- .../widget/sheet/WidgetOptionsBottomSheet.kt | 52 ++- .../maubis/markdown/inliner/TextInliner.kt | 1 - .../quicknote/database/RemoteController.kt | 16 +- .../database/RemoteDatabaseStateController.kt | 40 +- .../quicknote/database/RemoteUploadData.kt | 36 +- .../database/RemoteUploadDatabase.kt | 4 +- .../quicknote/drive/GDriveActivitySpecs.kt | 183 ++++---- .../quicknote/drive/GDriveLoginActivity.kt | 55 ++- .../quicknote/drive/GDriveLogoutActivity.kt | 32 +- .../drive/GDriveLogoutActivitySpecs.kt | 115 ++--- .../drive/GDrivePendingBottomSheet.kt | 271 ++++++------ .../quicknote/drive/GDriveRemoteDatabase.kt | 72 ++-- .../quicknote/drive/GDriveRemoteFolder.kt | 16 +- .../quicknote/drive/GDriveRemoteFolderBase.kt | 8 +- .../drive/GDriveRemoteImageFolder.kt | 14 +- .../quicknote/drive/GDriveServiceHelper.kt | 57 +-- .../firebase/activity/DataPolicyActivity.kt | 3 +- .../activity/FirebaseActivitySpecs.kt | 160 ++++--- .../activity/FirebaseLoginActivity.kt | 54 +-- .../activity/FirebaseRemovalActivity.kt | 67 ++- .../activity/FirebaseRemovalActivitySpecs.kt | 178 ++++---- .../firebase/activity/ForgetMeActivity.kt | 71 ++- .../quicknote/firebase/data/FirebaseFolder.kt | 20 +- .../quicknote/firebase/data/FirebaseNote.kt | 40 +- .../quicknote/firebase/data/FirebaseTag.kt | 4 +- .../firebase/data/FolderExtensions.kt | 10 +- .../quicknote/firebase/data/NoteExtensions.kt | 20 +- .../support/FirebaseFolderReference.kt | 9 +- .../firebase/support/FirebaseNoteReference.kt | 8 +- .../firebase/support/FirebaseTagReference.kt | 9 +- .../firebase/support/RemoteConfigFetcher.kt | 14 +- 165 files changed, 4561 insertions(+), 4142 deletions(-) diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt index e4407d82..bbab0193 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivity.kt @@ -26,7 +26,22 @@ import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.export.support.NoteExporter import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.main.HomeNavigationState -import com.maubis.scarlet.base.main.recycler.* +import com.maubis.scarlet.base.main.recycler.EmptyRecyclerItem +import com.maubis.scarlet.base.main.recycler.GenericRecyclerItem +import com.maubis.scarlet.base.main.recycler.getAppUpdateInformationItem +import com.maubis.scarlet.base.main.recycler.getBackupInformationItem +import com.maubis.scarlet.base.main.recycler.getInstallProInformationItem +import com.maubis.scarlet.base.main.recycler.getMigrateToProAppInformationItem +import com.maubis.scarlet.base.main.recycler.getReviewInformationItem +import com.maubis.scarlet.base.main.recycler.getSignInInformationItem +import com.maubis.scarlet.base.main.recycler.getThemeInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowAppUpdateInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowBackupInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowInstallProInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowMigrateToProAppInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowReviewInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowSignInformationItem +import com.maubis.scarlet.base.main.recycler.shouldShowThemeInformationItem import com.maubis.scarlet.base.main.sheets.WhatsNewBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteTrashSheet import com.maubis.scarlet.base.main.specs.MainActivityBottomBar @@ -50,10 +65,13 @@ import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Compani import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_HOME_ENABLED import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount -import com.maubis.scarlet.base.support.* +import com.maubis.scarlet.base.support.SearchConfig import com.maubis.scarlet.base.support.database.HouseKeeper import com.maubis.scarlet.base.support.database.HouseKeeperJob import com.maubis.scarlet.base.support.database.Migrator +import com.maubis.scarlet.base.support.filterDirectlyValidFolders +import com.maubis.scarlet.base.support.filterFolder +import com.maubis.scarlet.base.support.filterOutFolders import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.ToolbarColorConfig @@ -61,11 +79,17 @@ import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.sAutomaticTheme import com.maubis.scarlet.base.support.ui.setThemeFromSystem +import com.maubis.scarlet.base.support.unifiedSearchSynchronous +import com.maubis.scarlet.base.support.unifiedSearchWithoutFolder import com.maubis.scarlet.base.support.utils.shouldShowWhatsNewSheet import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.search_toolbar_main.* import kotlinx.android.synthetic.main.toolbar_trash_info.* -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext import java.util.concurrent.atomic.AtomicBoolean class MainActivity : SecuredActivity(), INoteOptionSheetActivity { @@ -134,30 +158,30 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { } }) tagAndColorPicker = TagsAndColorPickerViewHolder( - this, - tagsFlexBox, - { tag -> - val isTagSelected = config.tags.filter { it.uuid == tag.uuid }.isNotEmpty() - when (isTagSelected) { - true -> { - config.tags.removeAll { it.uuid == tag.uuid } - startSearch(searchBox.text.toString()) - tagAndColorPicker.notifyChanged() - } - false -> { - openTag(tag) - tagAndColorPicker.notifyChanged() - } + this, + tagsFlexBox, + { tag -> + val isTagSelected = config.tags.filter { it.uuid == tag.uuid }.isNotEmpty() + when (isTagSelected) { + true -> { + config.tags.removeAll { it.uuid == tag.uuid } + startSearch(searchBox.text.toString()) + tagAndColorPicker.notifyChanged() } - }, - { color -> - when (config.colors.contains(color)) { - true -> config.colors.remove(color) - false -> config.colors.add(color) + false -> { + openTag(tag) + tagAndColorPicker.notifyChanged() } - tagAndColorPicker.notifyChanged() - startSearch(searchBox.text.toString()) - }) + } + }, + { color -> + when (config.colors.contains(color)) { + true -> config.colors.remove(color) + false -> config.colors.add(color) + } + tagAndColorPicker.notifyChanged() + startSearch(searchBox.text.toString()) + }) } fun setupRecyclerView() { @@ -173,16 +197,16 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { adapter = NoteAppAdapter(this, staggeredView, isTablet) adapter.setExtra(adapterExtra) recyclerView = RecyclerViewBuilder(this) - .setView(this, R.id.recycler_view) - .setAdapter(adapter) - .setLayoutManager(getLayoutManager(staggeredView, isTablet)) - .build() + .setView(this, R.id.recycler_view) + .setAdapter(adapter) + .setLayoutManager(getLayoutManager(staggeredView, isTablet)) + .build() vSwipeToRefresh.setOnRefreshListener { when { instance.authenticator().isLoggedIn(this) - && !instance.authenticator().isLegacyLoggedIn() - && !lastSyncHappening.get() -> instance.authenticator().requestSync(true) + && !instance.authenticator().isLegacyLoggedIn() + && !lastSyncHappening.get() -> instance.authenticator().requestSync(true) else -> vSwipeToRefresh.isRefreshing = false } } @@ -289,43 +313,43 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { if (config.folders.isNotEmpty()) { val allNotes = unifiedSearchSynchronous(config) allItems.addAll(allNotes - .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } - .map { it.await() }) + .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } + .map { it.await() }) return allItems } val allNotes = unifiedSearchWithoutFolder(config) val directAcceptableFolders = filterDirectlyValidFolders(config) allItems.addAll(CoreConfig.foldersDb.getAll() - .map { - GlobalScope.async(Dispatchers.IO) { - val isDirectFolder = directAcceptableFolders.contains(it) - val notesCount = filterFolder(allNotes, it).size - if (config.hasFilter() && notesCount == 0 && !isDirectFolder) { - return@async null - } - - FolderRecyclerItem( - context = this@MainActivity, - folder = it, - click = { - config.folders.clear() - config.folders.add(it) - unifiedSearch() - notifyFolderChange() - }, - longClick = { - CreateOrEditFolderBottomSheet.openSheet(this@MainActivity, it, { _, _ -> setupData() }) - }, - selected = config.hasFolder(it), - contents = notesCount) - } - } - .map { it.await() } - .filterNotNull()) + .map { + GlobalScope.async(Dispatchers.IO) { + val isDirectFolder = directAcceptableFolders.contains(it) + val notesCount = filterFolder(allNotes, it).size + if (config.hasFilter() && notesCount == 0 && !isDirectFolder) { + return@async null + } + + FolderRecyclerItem( + context = this@MainActivity, + folder = it, + click = { + config.folders.clear() + config.folders.add(it) + unifiedSearch() + notifyFolderChange() + }, + longClick = { + CreateOrEditFolderBottomSheet.openSheet(this@MainActivity, it, { _, _ -> setupData() }) + }, + selected = config.hasFolder(it), + contents = notesCount) + } + } + .map { it.await() } + .filterNotNull()) allItems.addAll(filterOutFolders(allNotes) - .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } - .map { it.await() }) + .map { GlobalScope.async(Dispatchers.IO) { NoteRecyclerItem(this@MainActivity, it) } } + .map { it.await() }) return allItems } @@ -337,10 +361,12 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { } val folder = config.folders.first() - lithoPreBottomToolbar.addView(LithoView.create(componentContext, + lithoPreBottomToolbar.addView( + LithoView.create( + componentContext, MainActivityFolderBottomBar.create(componentContext) - .folder(folder) - .build())) + .folder(folder) + .build())) } fun notifyDisabledSync() { @@ -351,22 +377,22 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { } lithoPreBottomToolbar.addView(LithoView.create(componentContext, - MainActivityDisabledSync.create(componentContext) - .onClick { - instance.authenticator().openTransferDataActivity(componentContext.androidContext)?.run() - } - .build())) + MainActivityDisabledSync.create(componentContext) + .onClick { + instance.authenticator().openTransferDataActivity(componentContext.androidContext)?.run() + } + .build())) } fun notifySyncingInformation(isSyncHappening: Boolean, isSyncPending: Boolean) { val componentContext = ComponentContext(this) if (!instance.authenticator().isLoggedIn(this) - || instance.authenticator().isLegacyLoggedIn()) { + || instance.authenticator().isLegacyLoggedIn()) { return } if (lastSyncPending.getAndSet(isSyncPending) == isSyncPending - && lastSyncHappening.getAndSet(isSyncHappening) == isSyncHappening) { + && lastSyncHappening.getAndSet(isSyncHappening) == isSyncHappening) { return } @@ -380,19 +406,19 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { GlobalScope.launch(Dispatchers.Main) { lithoSyncingBottomToolbar.removeAllViews() lithoSyncingBottomToolbar.addView(LithoView.create(componentContext, - MainActivitySyncingNow.create(componentContext) - .isSyncHappening(isSyncHappening) - .onClick { - if (!lastSyncHappening.get()) { - instance.authenticator().requestSync(true) - } - } - .onLongClick { - if (!lastSyncHappening.get()) { - instance.authenticator().showPendingSync(this@MainActivity) - } - } - .build())) + MainActivitySyncingNow.create(componentContext) + .isSyncHappening(isSyncHappening) + .onClick { + if (!lastSyncHappening.get()) { + instance.authenticator().requestSync(true) + } + } + .onLongClick { + if (!lastSyncHappening.get()) { + instance.authenticator().showPendingSync(this@MainActivity) + } + } + .build())) if (!isSyncHappening && isSyncPending) { instance.authenticator().requestSync(false) } @@ -534,10 +560,12 @@ class MainActivity : SecuredActivity(), INoteOptionSheetActivity { fun setBottomToolbar() { val componentContext = ComponentContext(this) lithoBottomToolbar.removeAllViews() - lithoBottomToolbar.addView(LithoView.create(componentContext, + lithoBottomToolbar.addView( + LithoView.create( + componentContext, MainActivityBottomBar.create(componentContext) - .colorConfig(ToolbarColorConfig()) - .build())) + .colorConfig(ToolbarColorConfig()) + .build())) } /** diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt index dc30d3f7..75a37dea 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -2,7 +2,6 @@ package com.maubis.scarlet.base import android.content.Context import android.content.Intent -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.settings.sheet.ThemeColorPickerBottomSheet import com.maubis.scarlet.base.support.sheets.openSheet @@ -42,7 +41,8 @@ fun MainActivity.handleIntent() { fun MainActivity.performAction(action: MainActivityActions) { val activity = this when (action) { - MainActivityActions.NIL -> {} + MainActivityActions.NIL -> { + } MainActivityActions.COLOR_PICKER -> { openSheet(this, ThemeColorPickerBottomSheet().apply { this.onThemeChange = { theme -> diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 1ba4377c..39e06bf8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -5,7 +5,6 @@ import android.graphics.Typeface import android.support.v4.content.res.ResourcesCompat import android.support.v7.app.AppCompatActivity import com.github.ajalt.reprint.core.Reprint -import com.github.bijoysingh.starter.prefs.Store import com.maubis.markdown.MarkdownConfig.Companion.config import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance @@ -29,10 +28,12 @@ abstract class CoreConfig(context: Context) { init { Reprint.initialize(context) config.spanConfig.headingTypeface = ResourcesCompat.getFont(context, R.font.monserrat) - ?: Typeface.DEFAULT + ?: Typeface.DEFAULT FONT_MONSERRAT = config.spanConfig.headingTypeface - FONT_MONSERRAT_MEDIUM = ResourcesCompat.getFont(context, R.font.monserrat_medium) ?: Typeface.DEFAULT - FONT_MONSERRAT_BOLD = ResourcesCompat.getFont(context, R.font.monserrat_bold) ?: Typeface.DEFAULT + FONT_MONSERRAT_MEDIUM = ResourcesCompat.getFont(context, R.font.monserrat_medium) + ?: Typeface.DEFAULT + FONT_MONSERRAT_BOLD = ResourcesCompat.getFont(context, R.font.monserrat_bold) + ?: Typeface.DEFAULT FONT_OPEN_SANS = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 3115c7c3..43ed8190 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -2,8 +2,6 @@ package com.maubis.scarlet.base.config import android.content.Context import android.support.v7.app.AppCompatActivity -import com.github.bijoysingh.starter.prefs.Store -import com.github.bijoysingh.starter.prefs.VersionedStore import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.auth.NullAuthenticator import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher @@ -50,7 +48,7 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { override fun remoteConfigFetcher(): IRemoteConfigFetcher = NullRemoteConfigFetcher() override fun remoteDatabaseState(): IRemoteDatabaseState { - return object: IRemoteDatabaseState { + return object : IRemoteDatabaseState { override fun notifyInsert(data: Any, onExecution: () -> Unit) {} override fun notifyRemove(data: Any, onExecution: () -> Unit) {} } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/remote/RemoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/remote/RemoteConfig.kt index 209be365..0760e7e3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/remote/RemoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/remote/RemoteConfig.kt @@ -1,6 +1,5 @@ package com.maubis.scarlet.base.config.remote - class RemoteConfig( - val rc_lite_production_version: Int?, - val rc_full_production_version: Int?) + val rc_lite_production_version: Int?, + val rc_full_production_version: Int?) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt index a4037d22..26ad9b1f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/Format.kt @@ -74,7 +74,7 @@ fun sectionPreservingSort(formats: List): List { val nextItem = mutableFormats[index + 1] if (currentItem.formatType == FormatType.CHECKLIST_CHECKED - && nextItem.formatType == FormatType.CHECKLIST_UNCHECKED) { + && nextItem.formatType == FormatType.CHECKLIST_UNCHECKED) { Collections.swap(mutableFormats, index, index + 1) continue } @@ -85,7 +85,7 @@ fun sectionPreservingSort(formats: List): List { val nextItem = mutableFormats[index - 1] if (currentItem.formatType == FormatType.CHECKLIST_UNCHECKED - && nextItem.formatType == FormatType.CHECKLIST_CHECKED) { + && nextItem.formatType == FormatType.CHECKLIST_CHECKED) { Collections.swap(mutableFormats, index, index - 1) continue } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt index 7f727b30..d751f0cb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/format/FormatBuilder.kt @@ -39,7 +39,8 @@ class FormatBuilder { continue } - val moreFormats = format.text.toInternalFormats(arrayOf( + val moreFormats = format.text.toInternalFormats( + arrayOf( MarkdownSegmentType.CHECKLIST_CHECKED, MarkdownSegmentType.CHECKLIST_UNCHECKED)) extractedFormats.addAll(moreFormats) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt index bfdcffe0..e5e8cbde 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/MaterialNoteActor.kt @@ -15,7 +15,12 @@ import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.export.data.ExportableNote import com.maubis.scarlet.base.main.activity.WidgetConfigureActivity -import com.maubis.scarlet.base.note.* +import com.maubis.scarlet.base.note.deleteToSync +import com.maubis.scarlet.base.note.getFullText +import com.maubis.scarlet.base.note.getTitleForSharing +import com.maubis.scarlet.base.note.mark +import com.maubis.scarlet.base.note.save +import com.maubis.scarlet.base.note.saveWithoutSync import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.service.FloatingNoteService @@ -32,10 +37,10 @@ open class MaterialNoteActor(val note: Note) : INoteActor { override fun share(context: Context) { IntentUtils.ShareBuilder(context) - .setSubject(note.getTitleForSharing()) - .setText(note.getFullText()) - .setChooserText(context.getString(R.string.share_using)) - .share() + .setSubject(note.getTitleForSharing()) + .setText(note.getFullText()) + .setChooserText(context.getString(R.string.share_using)) + .share() } override fun popup(activity: AppCompatActivity) { @@ -93,7 +98,6 @@ open class MaterialNoteActor(val note: Note) : INoteActor { note.save(activity) } - override fun onlineDelete(context: Context) { folderSync?.remove(ExportableNote(note)) } diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt index 469453bd..ec58e522 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteExtensions.kt @@ -13,14 +13,14 @@ fun Note.isUnsaved(): Boolean { fun Note.isEqual(note: Note): Boolean { return TextUtils.areEqualNullIsEmpty(this.state, note.state) - && TextUtils.areEqualNullIsEmpty(this.description, note.description) - && TextUtils.areEqualNullIsEmpty(this.uuid, note.uuid) - && TextUtils.areEqualNullIsEmpty(this.tags, note.tags) - && this.timestamp.toLong() == note.timestamp.toLong() - && this.color.toInt() == note.color.toInt() - && this.locked == note.locked - && this.pinned == note.pinned - && this.folder == note.folder + && TextUtils.areEqualNullIsEmpty(this.description, note.description) + && TextUtils.areEqualNullIsEmpty(this.uuid, note.uuid) + && TextUtils.areEqualNullIsEmpty(this.tags, note.tags) + && this.timestamp.toLong() == note.timestamp.toLong() + && this.color.toInt() == note.color.toInt() + && this.locked == note.locked + && this.pinned == note.pinned + && this.folder == note.folder } /************************************************************************************** diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt index a2ef989c..5cb70670 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteImage.kt @@ -84,9 +84,10 @@ class NoteImage(context: Context) { } } - fun loadThumbnailFileToImageView(noteUUID: String, imageUuid: String, - image: ImageView, - callback: ImageLoadCallback? = null) { + fun loadThumbnailFileToImageView( + noteUUID: String, imageUuid: String, + image: ImageView, + callback: ImageLoadCallback? = null) { GlobalScope.launch { val thumbnailFile = ApplicationBase.sAppImageCache.thumbnailFile(noteUUID, imageUuid) val persistentFile = ApplicationBase.sAppImageCache.persistentFile(noteUUID, imageUuid) diff --git a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteReminder.kt b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteReminder.kt index 8a86b188..9dcfec2d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/core/note/NoteReminder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/core/note/NoteReminder.kt @@ -12,11 +12,10 @@ class NoteReminder() { var interval: ReminderInterval = ReminderInterval.ONCE var daysOfWeek: IntArray = intArrayOf() - constructor( - alarmTimestamp: Long, - interval: ReminderInterval, - daysOfWeek: IntArray) : this() { + alarmTimestamp: Long, + interval: ReminderInterval, + daysOfWeek: IntArray) : this() { this.alarmTimestamp = alarmTimestamp this.interval = interval this.daysOfWeek = daysOfWeek @@ -50,9 +49,10 @@ class NoteReminder() { } } -class Reminder(var uid: Int = 0, - var timestamp: Long = 0, - var interval: ReminderInterval = ReminderInterval.ONCE) { +class Reminder( + var uid: Int = 0, + var timestamp: Long = 0, + var interval: ReminderInterval = ReminderInterval.ONCE) { fun toCalendar(): Calendar { val calendar = Calendar.getInstance() diff --git a/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt index 99305cf2..79393ddb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/FoldersProvider.kt @@ -52,7 +52,7 @@ class FoldersProvider { fun search(string: String): List { maybeLoadFromDB() return folders.values - .filter { string.isBlank() || it.title.contains(string, true) } + .filter { string.isBlank() || it.title.contains(string, true) } } @Synchronized diff --git a/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt b/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt index 346773ac..ca7ac14f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/TagsProvider.kt @@ -52,7 +52,7 @@ class TagsProvider { fun search(string: String): List { maybeLoadFromDB() return tags.values - .filter { string.isBlank() || it.title.contains(string, true) } + .filter { string.isBlank() || it.title.contains(string, true) } } @Synchronized diff --git a/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt b/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt index 1f173206..3915c55a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/database/remote/IRemoteDatabaseUtils.kt @@ -44,7 +44,7 @@ object IRemoteDatabaseUtils { val notifiedTag = TagBuilder().copy(tag) val existingTag = CoreConfig.tagsDb.getByUUID(tag.uuid()) var isSameAsExisting = existingTag !== null - && TextUtils.areEqualNullIsEmpty(notifiedTag.title, existingTag.title) + && TextUtils.areEqualNullIsEmpty(notifiedTag.title, existingTag.title) if (existingTag === null) { notifiedTag.saveWithoutSync() @@ -62,10 +62,10 @@ object IRemoteDatabaseUtils { val notifiedFolder = FolderBuilder().copy(folder) val existingFolder = CoreConfig.foldersDb.getByUUID(folder.uuid()) var isSameAsExisting = existingFolder !== null - && TextUtils.areEqualNullIsEmpty(notifiedFolder.title, existingFolder.title) - && (notifiedFolder.color == existingFolder.color) - && (notifiedFolder.timestamp == existingFolder.timestamp) - && (notifiedFolder.updateTimestamp == existingFolder.updateTimestamp) + && TextUtils.areEqualNullIsEmpty(notifiedFolder.title, existingFolder.title) + && (notifiedFolder.color == existingFolder.color) + && (notifiedFolder.timestamp == existingFolder.timestamp) + && (notifiedFolder.updateTimestamp == existingFolder.updateTimestamp) if (existingFolder === null) { notifiedFolder.saveWithoutSync() diff --git a/base/src/main/java/com/maubis/scarlet/base/database/room/AppDatabase.java b/base/src/main/java/com/maubis/scarlet/base/database/room/AppDatabase.java index feb44cd3..391b0a62 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/room/AppDatabase.java +++ b/base/src/main/java/com/maubis/scarlet/base/database/room/AppDatabase.java @@ -104,10 +104,7 @@ public void migrate(SupportSQLiteDatabase database) { }; public static AppDatabase createDatabase(Context context) { - return Room.databaseBuilder(context, AppDatabase.class, "note-database") - .allowMainThreadQueries() - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13) - .build(); + return Room.databaseBuilder(context, AppDatabase.class, "note-database").allowMainThreadQueries().addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13).build(); } public abstract NoteDao notes(); diff --git a/base/src/main/java/com/maubis/scarlet/base/database/room/note/NoteDao.java b/base/src/main/java/com/maubis/scarlet/base/database/room/note/NoteDao.java index a0f776a4..39edc985 100644 --- a/base/src/main/java/com/maubis/scarlet/base/database/room/note/NoteDao.java +++ b/base/src/main/java/com/maubis/scarlet/base/database/room/note/NoteDao.java @@ -38,8 +38,7 @@ public interface NoteDao { @Query("SELECT * FROM note WHERE tags LIKE :uuidRegex ORDER BY pinned DESC, timestamp DESC") List getNoteByTag(String uuidRegex); - @Query("SELECT COUNT(*) FROM note WHERE tags LIKE :uuidRegex ORDER BY pinned DESC, timestamp " - + "DESC") + @Query("SELECT COUNT(*) FROM note WHERE tags LIKE :uuidRegex ORDER BY pinned DESC, timestamp " + "DESC") int getNoteCountByTag(String uuidRegex); @Query("SELECT * FROM note WHERE uid = :uid LIMIT 1") diff --git a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt index 0fd02a95..fcd9e70f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/activity/ImportNoteActivity.kt @@ -9,7 +9,6 @@ import android.widget.TextView import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.recycler.FileRecyclerItem import com.maubis.scarlet.base.export.support.NoteImporter @@ -21,7 +20,6 @@ import com.maubis.scarlet.base.support.utils.bind import java.io.File import java.io.FileReader - class ImportNoteActivity : SecuredActivity() { val adapter = NoteAppAdapter(this) @@ -36,9 +34,9 @@ class ImportNoteActivity : SecuredActivity() { setContentView(R.layout.activity_import_note_from_file) RecyclerViewBuilder(this) - .setView(this, R.id.recycler_view) - .setAdapter(adapter) - .build() + .setView(this, R.id.recycler_view) + .setAdapter(adapter) + .build() val activity = this backButton.setOnClickListener { onBackPressed() } @@ -68,8 +66,8 @@ class ImportNoteActivity : SecuredActivity() { MultiAsyncTask.execute(object : MultiAsyncTask.Task> { override fun run(): List { return NoteImporter().getImportableFiles() - .map { FileRecyclerItem(it.name, it.lastModified(), it.absolutePath, it) } - .sorted() + .map { FileRecyclerItem(it.name, it.lastModified(), it.absolutePath, it) } + .sorted() } override fun handle(result: List) { @@ -101,7 +99,7 @@ class ImportNoteActivity : SecuredActivity() { pageTitle.setTextColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setTextColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) importFile.setBackgroundResource( - if (sAppTheme.isNightTheme()) R.drawable.light_circular_border_bg - else R.drawable.dark_circular_border_bg) + if (sAppTheme.isNightTheme()) R.drawable.light_circular_border_bg + else R.drawable.dark_circular_border_bg) } } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt index d5a4febb..ad17e1b4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableData.kt @@ -16,48 +16,48 @@ import java.util.* * readable format (markdown) and meta data object */ class ExportableSplitNote( - val content: String, - val meta: ExportableNoteMeta) { + val content: String, + val meta: ExportableNoteMeta) { // Default failsafe constructor for Gson to use constructor() : this( - "", - ExportableNoteMeta()) + "", + ExportableNoteMeta()) } /** * Data class containing only the meta data for the note which makes it unique to Scarlet */ class ExportableNoteMeta( - val uuid: String, - val timestamp: Long, - val updateTimestamp: Long, - val color: Int, - val state: String, - val tags: String, - val locked: Boolean, - val pinned: Boolean, - val folder: String) { + val uuid: String, + val timestamp: Long, + val updateTimestamp: Long, + val color: Int, + val state: String, + val tags: String, + val locked: Boolean, + val pinned: Boolean, + val folder: String) { // Default failsafe constructor for Gson to use constructor() : this( - "invalid", - Calendar.getInstance().timeInMillis, - Calendar.getInstance().timeInMillis, - -0xff8695, - NoteState.DEFAULT.name, - "", - false, - false, - "") + "invalid", + Calendar.getInstance().timeInMillis, + Calendar.getInstance().timeInMillis, + -0xff8695, + NoteState.DEFAULT.name, + "", + false, + false, + "") } /** * Data class for the exportability of tags */ class ExportableTag( - var uuid: String, - var title: String + var uuid: String, + var title: String ) : Serializable, ITagContainer { override fun title(): String = title @@ -68,8 +68,8 @@ class ExportableTag( constructor() : this("invalid", "") constructor(tag: Tag) : this( - tag.uuid, - tag.title + tag.uuid, + tag.title ) companion object { @@ -83,8 +83,8 @@ class ExportableTag( fun fromJSONObjectV1(json: JSONObject): ExportableTag { return ExportableTag( - generateUUID(), - json["title"] as String) + generateUUID(), + json["title"] as String) } fun getBestPossibleTagObject(json: JSONObject): Tag { @@ -97,11 +97,11 @@ class ExportableTag( * Data class for the exportability of folder */ class ExportableFolder( - val uuid: String, - val title: String, - val timestamp: Long, - val updateTimestamp: Long, - val color: Int + val uuid: String, + val title: String, + val timestamp: Long, + val updateTimestamp: Long, + val color: Int ) : Serializable, IFolderContainer { override fun timestamp(): Long = timestamp override fun updateTimestamp(): Long = updateTimestamp @@ -110,17 +110,17 @@ class ExportableFolder( override fun uuid(): String = uuid constructor(folder: Folder) : this( - folder.uuid ?: "", - folder.title ?: "", - folder.timestamp ?: 0, - folder.updateTimestamp, - folder.color ?: 0) + folder.uuid ?: "", + folder.title ?: "", + folder.timestamp ?: 0, + folder.updateTimestamp, + folder.color ?: 0) // Default failsafe constructor for Gson to use constructor() : this( - "invalid", - "", - Calendar.getInstance().timeInMillis, - Calendar.getInstance().timeInMillis, - -0xff8695) + "invalid", + "", + Calendar.getInstance().timeInMillis, + Calendar.getInstance().timeInMillis, + -0xff8695) } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt index 3a5c5cb6..a65f67dd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableExtensions.kt @@ -53,21 +53,21 @@ fun Note.toExportedMarkdown(): String { fun Note.getExportableSplitNote(): ExportableSplitNote { return ExportableSplitNote( - toExportedMarkdown(), - getExportableNoteMeta()) + toExportedMarkdown(), + getExportableNoteMeta()) } fun Note.getExportableNoteMeta(): ExportableNoteMeta { return ExportableNoteMeta( - uuid, - timestamp, - updateTimestamp, - color, - state, - if (tags == null) "" else tags, - locked, - pinned, - folder + uuid, + timestamp, + updateTimestamp, + color, + state, + if (tags == null) "" else tags, + locked, + pinned, + folder ) } @@ -83,14 +83,13 @@ fun Note.mergeMetas(meta: ExportableNoteMeta) { folder = meta.folder } - fun Folder.getExportableFolder(): ExportableFolder { return ExportableFolder( - uuid, - title, - timestamp, - updateTimestamp, - color + uuid, + title, + timestamp, + updateTimestamp, + color ) } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFileFormat.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFileFormat.kt index fbfdd65e..49a92b6a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFileFormat.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableFileFormat.kt @@ -1,7 +1,7 @@ package com.maubis.scarlet.base.export.data class ExportableFileFormat( - val version: Int, - val notes: List, - val tags: List, - val folders: List?) \ No newline at end of file + val version: Int, + val notes: List, + val tags: List, + val folders: List?) \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt index 8584b0cf..20d4406d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/data/ExportableNote.kt @@ -14,15 +14,15 @@ import org.json.JSONObject import java.io.Serializable class ExportableNote( - var uuid: String, - var description: String, - var timestamp: Long, - var updateTimestamp: Long, - var color: Int, - var state: String, - var tags: String, - var meta: Map, - var folder: String + var uuid: String, + var description: String, + var timestamp: Long, + var updateTimestamp: Long, + var color: Int, + var state: String, + var tags: String, + var meta: Map, + var folder: String ) : Serializable, INoteContainer { override fun uuid(): String = uuid @@ -48,15 +48,15 @@ class ExportableNote( override fun folder(): String = folder constructor(note: Note) : this( - note.uuid, - note.description, - note.timestamp, - note.updateTimestamp, - note.color, - note.state, - note.tags ?: "", - emptyMap(), - note.folder + note.uuid, + note.description, + note.timestamp, + note.updateTimestamp, + note.color, + note.state, + note.tags ?: "", + emptyMap(), + note.folder ) fun saveIfNeeded(context: Context) { @@ -75,41 +75,41 @@ class ExportableNote( fun fromJSONObjectV2(json: JSONObject): ExportableNote { return ExportableNote( - generateUUID(), - json["description"] as String, - json["timestamp"] as Long, - json["timestamp"] as Long, - json["color"] as Int, - "", - "", - emptyMap(), - "") + generateUUID(), + json["description"] as String, + json["timestamp"] as Long, + json["timestamp"] as Long, + json["color"] as Int, + "", + "", + emptyMap(), + "") } fun fromJSONObjectV3(json: JSONObject): ExportableNote { return ExportableNote( - generateUUID(), - json["description"] as String, - json["timestamp"] as Long, - json["timestamp"] as Long, - json["color"] as Int, - json["state"] as String, - convertTagsJSONArrayToString(json["tags"] as JSONArray), - emptyMap(), - "") + generateUUID(), + json["description"] as String, + json["timestamp"] as Long, + json["timestamp"] as Long, + json["color"] as Int, + json["state"] as String, + convertTagsJSONArrayToString(json["tags"] as JSONArray), + emptyMap(), + "") } fun fromJSONObjectV4(json: JSONObject): ExportableNote { return ExportableNote( - json["uuid"] as String, - json["description"] as String, - json["timestamp"] as Long, - json["timestamp"] as Long, - json["color"] as Int, - json["state"] as String, - convertTagsJSONArrayToString(json["tags"] as JSONArray), - emptyMap(), - "") + json["uuid"] as String, + json["description"] as String, + json["timestamp"] as Long, + json["timestamp"] as Long, + json["color"] as Int, + json["state"] as String, + convertTagsJSONArrayToString(json["tags"] as JSONArray), + emptyMap(), + "") } private fun convertTagsJSONArrayToString(tags: JSONArray): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt index 1a5333e9..5c76f956 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt @@ -9,7 +9,6 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -43,8 +42,8 @@ class FileImportViewHolder(context: Context, root: View) (context as ImportNoteActivity).select(item) } root.setBackgroundColor( - if (item.selected) sAppTheme.get( - context, R.color.material_grey_100, R.color.dark_hint_text) else Color.TRANSPARENT) + if (item.selected) sAppTheme.get( + context, R.color.material_grey_100, R.color.dark_hint_text) else Color.TRANSPARENT) } private fun getPath(item: FileRecyclerItem): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileRecyclerItem.kt index e095d898..c9670b44 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileRecyclerItem.kt @@ -5,10 +5,11 @@ import com.maubis.scarlet.base.export.support.AUTO_BACKUP_FILENAME import com.maubis.scarlet.base.support.recycler.RecyclerItem import java.io.File -class FileRecyclerItem(val name: String, - val date: Long, - val path: String, - val file: File) : RecyclerItem(), Comparable { +class FileRecyclerItem( + val name: String, + val date: Long, + val path: String, + val file: File) : RecyclerItem(), Comparable { var selected = false override val type = Type.FILE diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/FolderRemoteDatabase.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/FolderRemoteDatabase.kt index b6f4c8fe..19534139 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/FolderRemoteDatabase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/FolderRemoteDatabase.kt @@ -33,23 +33,23 @@ class FolderRemoteDatabase(val weakContext: WeakReference) : IRemoteDat rootFolder = File(Environment.getExternalStorageDirectory(), sFolderSyncPath) val notesFolder = File(rootFolder, "notes") notesRemoteFolder = RemoteFolder( - notesFolder, - ExportableNote::class.java, - { it -> onRemoteInsert(it) }, - { it -> onRemoteRemove(ExportableNote(it, "", 0L, 0L, 0, NoteState.DEFAULT.name, "", emptyMap(), "")) }, - onNotesInit) + notesFolder, + ExportableNote::class.java, + { it -> onRemoteInsert(it) }, + { it -> onRemoteRemove(ExportableNote(it, "", 0L, 0L, 0, NoteState.DEFAULT.name, "", emptyMap(), "")) }, + onNotesInit) tagsRemoteFolder = RemoteFolder( - File(rootFolder, "tags"), - ExportableTag::class.java, - { it -> onRemoteInsert(it) }, - { it -> onRemoteRemove(ExportableTag(it, "")) }, - onTagsInit) + File(rootFolder, "tags"), + ExportableTag::class.java, + { it -> onRemoteInsert(it) }, + { it -> onRemoteRemove(ExportableTag(it, "")) }, + onTagsInit) foldersRemoteFolder = RemoteFolder( - File(rootFolder, "folders"), - ExportableFolder::class.java, - { it -> onRemoteInsert(it) }, - { it -> onRemoteRemove(ExportableFolder(it, "", 0L, 0L, 0)) }, - onFoldersInit) + File(rootFolder, "folders"), + ExportableFolder::class.java, + { it -> onRemoteInsert(it) }, + { it -> onRemoteRemove(ExportableFolder(it, "", 0L, 0L, 0)) }, + onFoldersInit) val context = weakContext.get() if (context !== null) { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt index 1d490b30..ddda8cba 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteFolder.kt @@ -12,11 +12,12 @@ import java.io.File const val LAST_MODIFIED_ERROR_MARGIN = 7 * 1000 * 60 * 60 * 24L -class RemoteFolder(val folder: File, - val klass: Class, - val onRemoteInsert: (T) -> Unit, - val onRemoteDelete: (String) -> Unit, - val onInitComplete: () -> Unit) { +class RemoteFolder( + val folder: File, + val klass: Class, + val onRemoteInsert: (T) -> Unit, + val onRemoteDelete: (String) -> Unit, + val onInitComplete: () -> Unit) { val deletedFolder = File(folder, "deleted") val uuids = HashSet() diff --git a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt index 204f7bcb..62a2aa4d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/remote/RemoteImagesFolder.kt @@ -76,7 +76,6 @@ class RemoteImagesFolder(context: Context, val folder: File) { val imageFiles = noteFolder.listFiles() ?: emptyArray() imageFiles.filter { it.isFile } - val imagesKnown = internalCache.imagesForNote(note.uuid()).map { it.name } imageFiles.forEach { if (!imagesKnown.contains(it.name)) { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt index bfd44671..cd5b2b4c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/BackupSettingsOptionsBottomSheet.kt @@ -21,7 +21,8 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = context as MainActivity val options = ArrayList() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.home_option_install_from_store, subtitle = R.string.home_option_install_from_store_subtitle, icon = R.drawable.ic_action_play, @@ -30,57 +31,57 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { dismiss() }, visible = FlavorUtils.isOpenSource() - )) + )) options.add(LithoOptionsItem( - title = R.string.home_option_export, - subtitle = R.string.home_option_export_subtitle, - icon = R.drawable.ic_export, - listener = { - val manager = PermissionUtils().getStoragePermissionManager(activity) - val hasAllPermissions = manager.hasAllPermissions() - when (hasAllPermissions) { - true -> { - openExportSheet(activity) - dismiss() - } - false -> { - openSheet(activity, PermissionBottomSheet()) - } + title = R.string.home_option_export, + subtitle = R.string.home_option_export_subtitle, + icon = R.drawable.ic_export, + listener = { + val manager = PermissionUtils().getStoragePermissionManager(activity) + val hasAllPermissions = manager.hasAllPermissions() + when (hasAllPermissions) { + true -> { + openExportSheet(activity) + dismiss() + } + false -> { + openSheet(activity, PermissionBottomSheet()) } } + } )) options.add(LithoOptionsItem( - title = R.string.home_option_import, - subtitle = R.string.home_option_import_subtitle, - icon = R.drawable.ic_import, - listener = { - val manager = PermissionUtils().getStoragePermissionManager(activity) - val hasAllPermissions = manager.hasAllPermissions() - when (hasAllPermissions) { - true -> { - IntentUtils.startActivity(activity, ImportNoteActivity::class.java) - dismiss() - } - false -> { - openSheet(activity, PermissionBottomSheet()) - } + title = R.string.home_option_import, + subtitle = R.string.home_option_import_subtitle, + icon = R.drawable.ic_import, + listener = { + val manager = PermissionUtils().getStoragePermissionManager(activity) + val hasAllPermissions = manager.hasAllPermissions() + when (hasAllPermissions) { + true -> { + IntentUtils.startActivity(activity, ImportNoteActivity::class.java) + dismiss() + } + false -> { + openSheet(activity, PermissionBottomSheet()) } } + } )) options.add(LithoOptionsItem( - title = R.string.import_export_layout_folder_sync, - subtitle = R.string.import_export_layout_folder_sync_details, - icon = R.drawable.icon_folder_sync, - listener = { - val manager = PermissionUtils().getStoragePermissionManager(activity) - val hasAllPermissions = manager.hasAllPermissions() - when (hasAllPermissions) { - true -> { - openSheet(activity, ExternalFolderSyncBottomSheet()) - } - false -> openSheet(activity, PermissionBottomSheet()) + title = R.string.import_export_layout_folder_sync, + subtitle = R.string.import_export_layout_folder_sync_details, + icon = R.drawable.icon_folder_sync, + listener = { + val manager = PermissionUtils().getStoragePermissionManager(activity) + val hasAllPermissions = manager.hasAllPermissions() + when (hasAllPermissions) { + true -> { + openSheet(activity, ExternalFolderSyncBottomSheet()) } + false -> openSheet(activity, PermissionBottomSheet()) } + } )) return options } @@ -91,8 +92,8 @@ class BackupSettingsOptionsBottomSheet : LithoOptionBottomSheet() { return } openUnlockSheet( - activity = activity as ThemedActivity, - onUnlockSuccess = { openSheet(activity, ExportNotesBottomSheet()) }, - onUnlockFailure = { openExportSheet(activity) }) + activity = activity as ThemedActivity, + onUnlockSuccess = { openSheet(activity, ExportNotesBottomSheet()) }, + onUnlockFailure = { openExportSheet(activity) }) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt index 2455524e..06598578 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt @@ -14,8 +14,17 @@ import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.export.support.* -import com.maubis.scarlet.base.support.sheets.* +import com.maubis.scarlet.base.export.support.GenericFileProvider +import com.maubis.scarlet.base.export.support.NoteExporter +import com.maubis.scarlet.base.export.support.PermissionUtils +import com.maubis.scarlet.base.export.support.sAutoBackupMode +import com.maubis.scarlet.base.export.support.sBackupLockedNotes +import com.maubis.scarlet.base.export.support.sBackupMarkdown +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.OptionItemLayout +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.specs.separatorSpec import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -42,90 +51,96 @@ class ExportNotesBottomSheet : LithoBottomSheet() { val filenameRender = "${file?.parentFile?.name}/${file?.name}" val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.import_export_layout_exporting) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .text(filenameRender) - .typeface(Typeface.MONOSPACE) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(separatorSpec(componentContext).alpha(0.5f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.import_export_layout_exporting) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .text(filenameRender) + .typeface(Typeface.MONOSPACE) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(separatorSpec(componentContext).alpha(0.5f)) getOptions(componentContext).forEach { if (it.visible) { component.child(OptionItemLayout.create(componentContext) - .option(it) - .onClick { - it.listener() - reset(componentContext.androidContext, dialog) - }) + .option(it) + .onClick { + it.listener() + reset(componentContext.androidContext, dialog) + }) } } component.child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.import_export_layout_exporting_done) - .onPrimaryClick { - GlobalScope.launch { - val notes = NoteExporter().getExportContent() - val success = NoteExporter().saveToManualExportFile(notes) - GlobalScope.launch(Dispatchers.Main) { - ToastHelper.show(activity, if (success) R.string.import_export_layout_exported else R.string.import_export_layout_export_failed) - dismiss() - } - } - } - .secondaryActionRes(R.string.import_export_layout_exporting_share) - .onSecondaryClick { - GlobalScope.launch { - val notes = NoteExporter().getExportContent() - NoteExporter().saveToManualExportFile(notes) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + GlobalScope.launch { + val notes = NoteExporter().getExportContent() + val success = NoteExporter().saveToManualExportFile(notes) + GlobalScope.launch(Dispatchers.Main) { + ToastHelper.show( + activity, if (success) R.string.import_export_layout_exported else R.string.import_export_layout_export_failed) + dismiss() + } + } + } + .secondaryActionRes(R.string.import_export_layout_exporting_share) + .onSecondaryClick { + GlobalScope.launch { + val notes = NoteExporter().getExportContent() + NoteExporter().saveToManualExportFile(notes) - if (file == null || !file.exists()) { - return@launch - } + if (file == null || !file.exists()) { + return@launch + } - val uri = FileProvider.getUriForFile(activity, GenericFileProvider.PROVIDER, file) + val uri = FileProvider.getUriForFile(activity, GenericFileProvider.PROVIDER, file) - val intent = Intent(Intent.ACTION_SEND) - intent.type = "text/plain" - intent.putExtra(Intent.EXTRA_STREAM, uri) - startActivity(Intent.createChooser(intent, getString(R.string.share_using))) + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_STREAM, uri) + startActivity(Intent.createChooser(intent, getString(R.string.share_using))) - GlobalScope.launch(Dispatchers.Main) { - dismiss() - } - } - } - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 8f)) + GlobalScope.launch(Dispatchers.Main) { + dismiss() + } + } + } + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } fun getOptions(componentContext: ComponentContext): List { val activity = componentContext.androidContext as MainActivity val options = ArrayList() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.home_option_export_markdown, subtitle = R.string.home_option_export_markdown_subtitle, icon = R.drawable.ic_markdown_logo, listener = { sBackupMarkdown = !sBackupMarkdown }, isSelectable = true, selected = sBackupMarkdown - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.import_export_locked, subtitle = R.string.import_export_locked_details, icon = R.drawable.ic_action_lock, listener = { sBackupLockedNotes = !sBackupLockedNotes }, isSelectable = true, selected = sBackupLockedNotes - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.home_option_auto_export, subtitle = R.string.home_option_auto_export_subtitle, icon = R.drawable.ic_time, @@ -144,7 +159,7 @@ class ExportNotesBottomSheet : LithoBottomSheet() { }, isSelectable = true, selected = sAutoBackupMode - )) + )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index ed79c12e..c3abaf93 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -7,9 +7,7 @@ import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge -import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.export.support.ExternalFolderSync @@ -23,74 +21,78 @@ import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.specs.separatorSpec import com.maubis.scarlet.base.support.ui.ThemeColorType -import com.maubis.scarlet.base.support.ui.ThemedActivity - class ExternalFolderSyncBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.import_export_layout_folder_sync_title) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .textRes(R.string.import_export_layout_folder_sync_description) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(separatorSpec(componentContext).alpha(0.5f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_xlarge) - .typeface(FONT_MONSERRAT) - .textRes(R.string.import_export_layout_folder_sync_folder) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .text(sFolderSyncPath) - .typeface(Typeface.MONOSPACE) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(separatorSpec(componentContext).alpha(0.5f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.import_export_layout_folder_sync_title) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .textRes(R.string.import_export_layout_folder_sync_description) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(separatorSpec(componentContext).alpha(0.5f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_xlarge) + .typeface(FONT_MONSERRAT) + .textRes(R.string.import_export_layout_folder_sync_folder) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .text(sFolderSyncPath) + .typeface(Typeface.MONOSPACE) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(separatorSpec(componentContext).alpha(0.5f)) getOptions().forEach { if (it.visible) { component.child(OptionItemLayout.create(componentContext) - .option(it) - .onClick { - it.listener() - reset(componentContext.androidContext, dialog) - }) + .option(it) + .onClick { + it.listener() + reset(componentContext.androidContext, dialog) + }) } } component.child(BottomSheetBar.create(componentContext) - .primaryActionRes(if (sExternalFolderSync) R.string.import_export_layout_folder_sync_disable else R.string.import_export_layout_folder_sync_enable) - .isActionNegative(sExternalFolderSync) - .onPrimaryClick { - sExternalFolderSync = !sExternalFolderSync - ExternalFolderSync.enable(componentContext.androidContext, sExternalFolderSync) - reset(componentContext.androidContext, dialog) - } - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 8f)) + .primaryActionRes( + if (sExternalFolderSync) R.string.import_export_layout_folder_sync_disable else R.string.import_export_layout_folder_sync_enable) + .isActionNegative(sExternalFolderSync) + .onPrimaryClick { + sExternalFolderSync = !sExternalFolderSync + ExternalFolderSync.enable(componentContext.androidContext, sExternalFolderSync) + reset(componentContext.androidContext, dialog) + } + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } fun getOptions(): List { val options = ArrayList() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.import_export_locked, subtitle = R.string.import_export_locked_details, icon = R.drawable.ic_action_lock, listener = { sFolderSyncBackupLocked = !sFolderSyncBackupLocked }, isSelectable = true, selected = sFolderSyncBackupLocked - )) + )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt index fa6956c4..2c714e0e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt @@ -7,7 +7,6 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -20,27 +19,29 @@ class PermissionBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val activity = context as ThemedActivity val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.permission_layout_give_permission) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .textRes(R.string.permission_layout_give_permission_details) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.permission_layout_give_permission_ok) - .onPrimaryClick { - val manager = PermissionUtils().getStoragePermissionManager(activity) - manager.requestPermissions() - dismiss() - }.secondaryActionRes(R.string.delete_sheet_delete_trash_no) - .onSecondaryClick { - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.permission_layout_give_permission) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .textRes(R.string.permission_layout_give_permission_details) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.permission_layout_give_permission_ok) + .onPrimaryClick { + val manager = PermissionUtils().getStoragePermissionManager(activity) + manager.requestPermissions() + dismiss() + }.secondaryActionRes(R.string.delete_sheet_delete_trash_no) + .onSecondaryClick { + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt index ded6c021..8a41b8ca 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/ExternalFolderSync.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference - const val KEY_EXTERNAL_FOLDER_SYNC_ENABLED = "external_folder_sync_enabled" const val KEY_EXTERNAL_FOLDER_SYNC_LAST_SCAN = "external_folder_sync_last_sync" const val KEY_EXTERNAL_FOLDER_SYNC_BACKUP_LOCKED = "external_folder_sync_backup_locked" @@ -42,7 +41,7 @@ object ExternalFolderSync { fun hasPermission(context: Context): Boolean { return !(OsVersionUtils.requiresPermissions() - && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) + && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) } fun enable(context: Context, enabled: Boolean) { @@ -64,21 +63,21 @@ object ExternalFolderSync { fun loadFirstTime() { folderSync?.init( - { - ApplicationBase.instance.notesDatabase().getAll().forEach { - folderSync?.insert(ExportableNote(it)) - } - }, - { - ApplicationBase.instance.tagsDatabase().getAll().forEach { - folderSync?.insert(ExportableTag(it)) - } - }, - { - ApplicationBase.instance.foldersDatabase().getAll().forEach { - folderSync?.insert(ExportableFolder(it)) - } - }) + { + ApplicationBase.instance.notesDatabase().getAll().forEach { + folderSync?.insert(ExportableNote(it)) + } + }, + { + ApplicationBase.instance.tagsDatabase().getAll().forEach { + folderSync?.insert(ExportableTag(it)) + } + }, + { + ApplicationBase.instance.foldersDatabase().getAll().forEach { + folderSync?.insert(ExportableFolder(it)) + } + }) } fun setup(context: Context) { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt index b2497dfe..8c9c8e83 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteExporter.kt @@ -8,7 +8,11 @@ import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.CoreConfig.Companion.foldersDb import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb -import com.maubis.scarlet.base.export.data.* +import com.maubis.scarlet.base.export.data.ExportableFileFormat +import com.maubis.scarlet.base.export.data.ExportableFolder +import com.maubis.scarlet.base.export.data.ExportableNote +import com.maubis.scarlet.base.export.data.ExportableTag +import com.maubis.scarlet.base.export.data.toExportedMarkdown import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FILENAME import com.maubis.scarlet.base.export.sheet.NOTES_EXPORT_FOLDER import com.maubis.scarlet.base.support.utils.sDateFormat @@ -47,9 +51,9 @@ class NoteExporter() { } val notes = notesDb - .getAll() - .filter { sBackupLockedNotes || !it.locked } - .map { ExportableNote(it) } + .getAll() + .filter { sBackupLockedNotes || !it.locked } + .map { ExportableNote(it) } val tags = tagsDb.getAll().map { ExportableTag(it) } val folders = foldersDb.getAll().map { ExportableFolder(it) } val fileContent = ExportableFileFormat(EXPORT_VERSION, notes, tags, folders) @@ -59,11 +63,11 @@ class NoteExporter() { private fun getMarkdownExportContent(): String { var totalText = "$EXPORT_NOTE_SEPARATOR\n\n" notesDb.getAll() - .map { it.toExportedMarkdown() } - .forEach { - totalText += it - totalText += "\n\n$EXPORT_NOTE_SEPARATOR\n\n" - } + .map { it.toExportedMarkdown() } + .forEach { + totalText += it + totalText += "\n\n$EXPORT_NOTE_SEPARATOR\n\n" + } return totalText } @@ -79,19 +83,19 @@ class NoteExporter() { } val exportFile = getOrCreateFileForExport( - "$AUTO_BACKUP_FILENAME ${sDateFormat.getDateForBackup()}") + "$AUTO_BACKUP_FILENAME ${sDateFormat.getDateForBackup()}") if (exportFile === null) { return@execute } saveToFile(exportFile, getExportContent()) sAppPreferences - .put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) + .put(KEY_AUTO_BACKUP_LAST_TIMESTAMP, System.currentTimeMillis()) } } fun getOrCreateManualExportFile(): File? { return getOrCreateFileForExport( - "$NOTES_EXPORT_FILENAME ${sDateFormat.getTimestampForBackup()}") + "$NOTES_EXPORT_FILENAME ${sDateFormat.getTimestampForBackup()}") } fun getOrCreateFileForExport(filename: String): File? { diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt index e22b46fa..f897e83f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/NoteImporter.kt @@ -66,14 +66,14 @@ class NoteImporter() { private fun importNoteFallback(content: String, context: Context) { content - .split(EXPORT_NOTE_SEPARATOR) - .map { - it.trim() - } - .filter { it.isNotBlank() } - .forEach { - NoteBuilder().gen("", it).save(context) - } + .split(EXPORT_NOTE_SEPARATOR) + .map { + it.trim() + } + .filter { it.isNotBlank() } + .forEach { + NoteBuilder().gen("", it).save(context) + } } fun getImportableFiles(): List { @@ -109,7 +109,6 @@ class NoteImporter() { return files } - fun readFileInputStream(inputStreamReader: InputStreamReader): String { lateinit var reader: BufferedReader try { @@ -134,7 +133,7 @@ class NoteImporter() { private fun isValidFile(filePath: String, validExtension: String): Boolean { return filePath.endsWith("." + validExtension) - || filePath.endsWith("." + validExtension.toUpperCase()) + || filePath.endsWith("." + validExtension.toUpperCase()) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/support/PermissionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/export/support/PermissionUtils.kt index 214f21f5..d6d378ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/support/PermissionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/support/PermissionUtils.kt @@ -7,7 +7,8 @@ import com.github.bijoysingh.starter.util.PermissionManager class PermissionUtils() { fun getStoragePermissionManager(activity: AppCompatActivity): PermissionManager { val manager = PermissionManager(activity) - manager.setPermissions(arrayOf( + manager.setPermissions( + arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) return manager diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt index 1c0d487b..453b808b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/OpenTextIntentOrFileActivity.kt @@ -28,7 +28,6 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.utils.bind import java.io.InputStreamReader - const val KEEP_PACKAGE = "com.google.android.keep" const val INTENT_KEY_DIRECT_NOTES_TRANSFER = "direct_notes_transfer" @@ -59,7 +58,6 @@ class OpenTextIntentOrFileActivity : SecuredActivity() { setView() notifyThemeChange() - val spannable = SpannableString(contentText) spannable.setFormats(Markdown.getSpanInfo(contentText).spans) content.setText(spannable, TextView.BufferType.SPANNABLE) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt index 432463a7..7b6ebecb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/activity/WidgetConfigureActivity.kt @@ -35,8 +35,8 @@ class WidgetConfigureActivity : SelectableNotesActivityBase(), INoteSelectorActi val extras = intent.extras if (extras != null) { appWidgetId = extras.getInt( - AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID) + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID) } if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { @@ -100,7 +100,7 @@ class WidgetConfigureActivity : SelectableNotesActivityBase(), INoteSelectorActi private fun notifyNoteChangeBroadcast(context: Context, note: Note): Intent? { val application: Application = context.applicationContext as Application val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( - ComponentName(application, NoteWidgetProvider::class.java)) + ComponentName(application, NoteWidgetProvider::class.java)) val widgets = ApplicationBase.instance.database().widgets().getByNote(note.uuid) val widgetIds = ArrayList() diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt index b2049092..2e613c68 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/EmptyRecyclerHolder.kt @@ -14,8 +14,8 @@ class EmptyRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder setFullSpan() itemView.setOnClickListener { val newNoteIntent = CreateNoteActivity.getNewNoteIntent( - context, - folder = (context as MainActivity).config.folders.firstOrNull()?.uuid ?: "" + context, + folder = (context as MainActivity).config.folders.firstOrNull()?.uuid ?: "" ) context.startActivity(newNoteIntent) } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt index 670fa2a0..c2fde4a2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerItem.kt @@ -44,22 +44,22 @@ fun shouldShowAppUpdateInformationItem(): Boolean { fun getAppUpdateInformationItem(context: Context): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_info, - R.string.information_card_title, - R.string.information_new_app_update + R.drawable.ic_info, + R.string.information_card_title, + R.string.information_new_app_update ) { IntentUtils.openAppPlayStore(context) } } fun shouldShowReviewInformationItem(): Boolean { return probability(0.01f) - && !sAppPreferences.get(KEY_INFO_RATE_AND_REVIEW, false) + && !sAppPreferences.get(KEY_INFO_RATE_AND_REVIEW, false) } fun getReviewInformationItem(context: Context): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_rating, - R.string.home_option_rate_and_review, - R.string.home_option_rate_and_review_subtitle + R.drawable.ic_rating, + R.string.home_option_rate_and_review, + R.string.home_option_rate_and_review_subtitle ) { sAppPreferences.put(KEY_INFO_RATE_AND_REVIEW, true) IntentUtils.openAppPlayStore(context) @@ -68,14 +68,14 @@ fun getReviewInformationItem(context: Context): InformationRecyclerItem { fun shouldShowThemeInformationItem(): Boolean { return probability(0.01f) - && !sAppPreferences.get(KEY_THEME_OPTIONS, false) + && !sAppPreferences.get(KEY_THEME_OPTIONS, false) } fun getThemeInformationItem(activity: MainActivity): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_action_grid, - R.string.home_option_ui_experience, - R.string.home_option_ui_experience_subtitle + R.drawable.ic_action_grid, + R.string.home_option_ui_experience, + R.string.home_option_ui_experience_subtitle ) { sAppPreferences.put(KEY_THEME_OPTIONS, true) UISettingsOptionsBottomSheet.openSheet(activity) @@ -84,32 +84,31 @@ fun getThemeInformationItem(activity: MainActivity): InformationRecyclerItem { fun shouldShowBackupInformationItem(): Boolean { return probability(0.01f) - && !sAppPreferences.get(KEY_BACKUP_OPTIONS, false) + && !sAppPreferences.get(KEY_BACKUP_OPTIONS, false) } fun getBackupInformationItem(activity: MainActivity): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_export, - R.string.home_option_backup_options, - R.string.home_option_backup_options_subtitle + R.drawable.ic_export, + R.string.home_option_backup_options, + R.string.home_option_backup_options_subtitle ) { sAppPreferences.put(KEY_BACKUP_OPTIONS, true) openSheet(activity, BackupSettingsOptionsBottomSheet()) } } - fun shouldShowInstallProInformationItem(): Boolean { return probability(0.01f) - && sAppPreferences.get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT - && !FlavorUtils.isPro() + && sAppPreferences.get(KEY_INFO_INSTALL_PRO_v2, 0) < KEY_INFO_INSTALL_PRO_MAX_COUNT + && !FlavorUtils.isPro() } fun getInstallProInformationItem(context: Context): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_favorite_white_48dp, - R.string.install_pro_app, - R.string.information_install_pro + R.drawable.ic_favorite_white_48dp, + R.string.install_pro_app, + R.string.information_install_pro ) { notifyProUpsellShown() IntentUtils.openAppPlayStore(context, "com.bijoysingh.quicknote.pro") @@ -125,14 +124,14 @@ fun shouldShowSignInformationItem(context: Context): Boolean { return true } return probability(0.01f) - && !sAppPreferences.get(KEY_INFO_SIGN_IN, false) + && !sAppPreferences.get(KEY_INFO_SIGN_IN, false) } fun getSignInInformationItem(context: Context): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_sign_in_options, - R.string.home_option_login_with_app, - R.string.home_option_login_with_app_subtitle + R.drawable.ic_sign_in_options, + R.string.home_option_login_with_app, + R.string.home_option_login_with_app_subtitle ) { ApplicationBase.instance.authenticator().openLoginActivity(context)?.run() notifyProUpsellShown() @@ -144,25 +143,24 @@ fun notifyProUpsellShown() { sAppPreferences.put(KEY_INFO_INSTALL_PRO_v2, proUpsellCount + 1) } - fun shouldShowMigrateToProAppInformationItem(context: Context): Boolean { return FlavorUtils.isLite() - && FlavorUtils.hasProAppInstalled(context) - && !sAppPreferences.get(KEY_MIGRATE_TO_PRO_SUCCESS, false) + && FlavorUtils.hasProAppInstalled(context) + && !sAppPreferences.get(KEY_MIGRATE_TO_PRO_SUCCESS, false) } fun getMigrateToProAppInformationItem(context: Context): InformationRecyclerItem { return InformationRecyclerItem( - R.drawable.ic_import, - R.string.home_option_migrate_to_pro, - R.string.home_option_migrate_to_pro_details + R.drawable.ic_import, + R.string.home_option_migrate_to_pro, + R.string.home_option_migrate_to_pro_details ) { GlobalScope.launch(Dispatchers.Main) { val notes = GlobalScope.async(Dispatchers.IO) { NoteExporter().getExportContent() } val intent = Intent(Intent.ACTION_SEND) - .putExtra(INTENT_KEY_DIRECT_NOTES_TRANSFER, notes.await()) - .setType("text/plain") - .setPackage(FlavorUtils.PRO_APP_PACKAGE_NAME) + .putExtra(INTENT_KEY_DIRECT_NOTES_TRANSFER, notes.await()) + .setType("text/plain") + .setPackage(FlavorUtils.PRO_APP_PACKAGE_NAME) try { context.startActivity(intent) sAppPreferences.put(KEY_MIGRATE_TO_PRO_SUCCESS, true) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index 5fab3154..2b4398e8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -11,7 +11,6 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.settings.sheet.InternalSettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt index 381fb39a..03fc4f63 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/AlertBottomSheet.kt @@ -8,7 +8,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig @@ -25,25 +24,25 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity data class AlertSheetConfig( - val title: Int = R.string.delete_sheet_are_you_sure, - val description: Int = R.string.delete_sheet_delete_note_permanently, - val positiveText: Int = R.string.delete_sheet_delete_trash_yes, - val negativeText: Int = R.string.delete_sheet_delete_trash_no, - val onPositiveClick: () -> Unit = {}, - val onNegativeClick: () -> Unit = {}) + val title: Int = R.string.delete_sheet_are_you_sure, + val description: Int = R.string.delete_sheet_delete_note_permanently, + val positiveText: Int = R.string.delete_sheet_delete_trash_yes, + val negativeText: Int = R.string.delete_sheet_delete_trash_no, + val onPositiveClick: () -> Unit = {}, + val onNegativeClick: () -> Unit = {}) fun openDeleteNotePermanentlySheet(activity: ThemedActivity, note: Note, onDelete: () -> Unit) { openSheet(activity, AlertBottomSheet().apply { this.config = AlertSheetConfig( - title = R.string.delete_sheet_are_you_sure, - description = R.string.delete_sheet_delete_note_permanently, - positiveText = R.string.delete_sheet_delete_trash_yes, - negativeText = R.string.delete_sheet_delete_trash_no, - onPositiveClick = { - note.delete(activity) - onDelete() - }, - onNegativeClick = {}) + title = R.string.delete_sheet_are_you_sure, + description = R.string.delete_sheet_delete_note_permanently, + positiveText = R.string.delete_sheet_delete_trash_yes, + negativeText = R.string.delete_sheet_delete_trash_no, + onPositiveClick = { + note.delete(activity) + onDelete() + }, + onNegativeClick = {}) }) } @@ -52,27 +51,29 @@ class AlertBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(config.title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .textRes(config.description) - .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(config.positiveText) - .onPrimaryClick { - config.onPositiveClick() - dismiss() - }.secondaryActionRes(config.negativeText) - .onSecondaryClick { - config.onNegativeClick() - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(config.title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .textRes(config.description) + .marginDip(YogaEdge.BOTTOM, 16f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(config.positiveText) + .onPrimaryClick { + config.onPositiveClick() + dismiss() + }.secondaryActionRes(config.negativeText) + .onSecondaryClick { + config.onNegativeClick() + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } @@ -80,28 +81,28 @@ class AlertBottomSheet : LithoBottomSheet() { fun openDeleteAllXSheet(activity: MainActivity, subtitle: Int, onSuccess: () -> Unit) { openSheet(activity, AlertBottomSheet().apply { this.config = AlertSheetConfig( - title = R.string.delete_sheet_are_you_sure, - description = subtitle, - positiveText = R.string.delete_sheet_delete_trash_yes, - negativeText = R.string.delete_sheet_delete_trash_no, - onPositiveClick = { - onSuccess() - }, - onNegativeClick = {}) + title = R.string.delete_sheet_are_you_sure, + description = subtitle, + positiveText = R.string.delete_sheet_delete_trash_yes, + negativeText = R.string.delete_sheet_delete_trash_no, + onPositiveClick = { + onSuccess() + }, + onNegativeClick = {}) }) } fun openDeleteFormatDialog(activity: ViewAdvancedNoteActivity, format: Format) { openSheet(activity, AlertBottomSheet().apply { this.config = AlertSheetConfig( - title = R.string.delete_sheet_are_you_sure, - description = R.string.image_delete_all_devices, - positiveText = R.string.delete_sheet_delete_trash_yes, - negativeText = R.string.delete_sheet_delete_trash_no, - onPositiveClick = { - activity.deleteFormat(format) - }, - onNegativeClick = {}) + title = R.string.delete_sheet_are_you_sure, + description = R.string.image_delete_all_devices, + positiveText = R.string.delete_sheet_delete_trash_yes, + negativeText = R.string.delete_sheet_delete_trash_no, + onPositiveClick = { + activity.deleteFormat(format) + }, + onNegativeClick = {}) }) } @@ -110,33 +111,32 @@ var sImageSyncNoticeShown: Int get() = sAppPreferences.get(STORE_KEY_IMAGE_SYNC_NOTICE, 0) set(value) = sAppPreferences.put(STORE_KEY_IMAGE_SYNC_NOTICE, value) - fun openImageNotSynced(activity: ThemedActivity) { openSheet(activity, AlertBottomSheet().apply { this.config = AlertSheetConfig( - title = R.string.image_not_uploaded, - description = R.string.image_not_uploaded_details, - positiveText = R.string.image_not_uploaded_i_understand, - negativeText = R.string.delete_sheet_delete_trash_no, - onPositiveClick = { sImageSyncNoticeShown = 1 }, - onNegativeClick = {}) + title = R.string.image_not_uploaded, + description = R.string.image_not_uploaded_details, + positiveText = R.string.image_not_uploaded_i_understand, + negativeText = R.string.delete_sheet_delete_trash_no, + onPositiveClick = { sImageSyncNoticeShown = 1 }, + onNegativeClick = {}) }) } fun openDeleteTrashSheet(activity: MainActivity) { openSheet(activity, AlertBottomSheet().apply { this.config = AlertSheetConfig( - title = R.string.delete_sheet_are_you_sure, - description = R.string.delete_sheet_delete_trash, - positiveText = R.string.delete_sheet_delete_trash_yes, - negativeText = R.string.delete_sheet_delete_trash_no, - onPositiveClick = { - val notes = CoreConfig.notesDb.getByNoteState(arrayOf(NoteState.TRASH.name)) - for (note in notes) { - note.delete(activity) - } - activity.setupData() - }, - onNegativeClick = {}) + title = R.string.delete_sheet_are_you_sure, + description = R.string.delete_sheet_delete_trash, + positiveText = R.string.delete_sheet_delete_trash_yes, + negativeText = R.string.delete_sheet_delete_trash_no, + onPositiveClick = { + val notes = CoreConfig.notesDb.getByNoteState(arrayOf(NoteState.TRASH.name)) + for (note in notes) { + note.delete(activity) + } + activity.setupData() + }, + onNegativeClick = {}) }) } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt index 8b0b75e5..ed432ba9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt @@ -11,7 +11,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -23,33 +22,35 @@ class ExceptionBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.exception_sheet_title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_small) - .text(Markdown.render("```\n${Log.getStackTraceString(exception)}\n```", true)) - .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.exception_sheet_crash_app) - .onPrimaryClick { - throw exception - }.secondaryActionRes(R.string.exception_sheet_mail) - .onSecondaryClick { - try { - val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:team.thecodershub@gmail.com")) - intent.putExtra(Intent.EXTRA_SUBJECT, "[Exception] The application threw an exception") - intent.putExtra(Intent.EXTRA_TEXT, "Hi, my app threw this exception\n${Log.getStackTraceString(exception)}") - startActivity(Intent.createChooser(intent, "Send email to developer...")) - } catch (exception: Exception) { - // Ignore this one ;) - } - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.exception_sheet_title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_small) + .text(Markdown.render("```\n${Log.getStackTraceString(exception)}\n```", true)) + .marginDip(YogaEdge.BOTTOM, 16f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.exception_sheet_crash_app) + .onPrimaryClick { + throw exception + }.secondaryActionRes(R.string.exception_sheet_mail) + .onSecondaryClick { + try { + val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:team.thecodershub@gmail.com")) + intent.putExtra(Intent.EXTRA_SUBJECT, "[Exception] The application threw an exception") + intent.putExtra(Intent.EXTRA_TEXT, "Hi, my app threw this exception\n${Log.getStackTraceString(exception)}") + startActivity(Intent.createChooser(intent, "Send email to developer...")) + } catch (exception: Exception) { + // Ignore this one ;) + } + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt index 444fede6..98d1e586 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt @@ -2,7 +2,11 @@ package com.maubis.scarlet.base.main.sheets import android.app.Dialog import android.graphics.Typeface -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -12,7 +16,6 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.tag.TagBuilder @@ -21,17 +24,22 @@ import com.maubis.scarlet.base.main.HomeNavigationState import com.maubis.scarlet.base.note.tag.sheet.CreateOrEditTagBottomSheet import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet import com.maubis.scarlet.base.support.SearchConfig -import com.maubis.scarlet.base.support.sheets.* +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoLabelOptionsItem +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.OptionItemLayout +import com.maubis.scarlet.base.support.sheets.OptionLabelItemLayout +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType class LithoTagOptionsItem( - val tag: Tag, - val usages: Int = 0, - val isSelected: Boolean = false, - val isEditable: Boolean = false, - val editListener: () -> Unit = {}, - val listener: () -> Unit = {}) { + val tag: Tag, + val usages: Int = 0, + val isSelected: Boolean = false, + val isEditable: Boolean = false, + val editListener: () -> Unit = {}, + val listener: () -> Unit = {}) { } @LayoutSpec @@ -67,31 +75,33 @@ object TagItemLayoutSpec { } val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 12f) - .child( - RoundIcon.create(context) - .iconRes(icon) - .bgColor(bgColor) - .iconColor(titleColor) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .bgAlpha(bgAlpha) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.END, 16f)) - .child(Text.create(context) - .flexGrow(1f) - .text(option.tag.title) - .textSizeRes(R.dimen.font_size_normal) - .typeface(typeface) - .textStyle(Typeface.BOLD) - .textColor(textColor)) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + RoundIcon.create(context) + .iconRes(icon) + .bgColor(bgColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(bgAlpha) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.END, 16f)) + .child( + Text.create(context) + .flexGrow(1f) + .text(option.tag.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(typeface) + .textStyle(Typeface.BOLD) + .textColor(textColor)) if (option.usages > 0) { - row.child(Text.create(context) + row.child( + Text.create(context) .text("${option.usages}") .textSizeRes(R.dimen.font_size_normal) .textColor(titleColor) @@ -100,16 +110,16 @@ object TagItemLayoutSpec { if (option.isEditable) { row.child(RoundIcon.create(context) - .iconRes(R.drawable.ic_edit_white_48dp) - .bgColor(titleColor) - .bgAlpha(15) - .iconAlpha(0.9f) - .iconColor(titleColor) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .onClick { option.editListener() } - .isClickDisabled(false) - .marginDip(YogaEdge.START, 12f)) + .iconRes(R.drawable.ic_edit_white_48dp) + .bgColor(titleColor) + .bgAlpha(15) + .iconAlpha(0.9f) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .onClick { option.editListener() } + .isClickDisabled(false) + .marginDip(YogaEdge.START, 12f)) } row.clickHandler(OptionItemLayout.onItemClick(context)) @@ -122,48 +132,49 @@ object TagItemLayoutSpec { } } - class HomeOptionsBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val activity = context as MainActivity val options = getOptions() val component = Column.create(componentContext) - .widthPercent(100f) - .child(Column.create(componentContext) - .paddingDip(YogaEdge.TOP, 20f) - .paddingDip(YogaEdge.BOTTOM, 20f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child( - Row.create(componentContext) - .child(OptionLabelItemLayout.create(componentContext).option(options[0]).onClick { options[0].listener() }) - .child(OptionLabelItemLayout.create(componentContext).option(options[1]).onClick { options[1].listener() }) - .child(OptionLabelItemLayout.create(componentContext).option(options[2]).onClick { options[2].listener() }) - ) - .child( - Row.create(componentContext) - .child(OptionLabelItemLayout.create(componentContext).option(options[3]).onClick { options[3].listener() }) - .child(OptionLabelItemLayout.create(componentContext).option(options[4]).onClick { options[4].listener() }) - .child(OptionLabelItemLayout.create(componentContext).option(options[5]).onClick { options[5].listener() }) - )) + .widthPercent(100f) + .child( + Column.create(componentContext) + .paddingDip(YogaEdge.TOP, 20f) + .paddingDip(YogaEdge.BOTTOM, 20f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + Row.create(componentContext) + .child(OptionLabelItemLayout.create(componentContext).option(options[0]).onClick { options[0].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[1]).onClick { options[1].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[2]).onClick { options[2].listener() }) + ) + .child( + Row.create(componentContext) + .child(OptionLabelItemLayout.create(componentContext).option(options[3]).onClick { options[3].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[4]).onClick { options[4].listener() }) + .child(OptionLabelItemLayout.create(componentContext).option(options[5]).onClick { options[5].listener() }) + )) val tagsComponent = Column.create(componentContext) - .paddingDip(YogaEdge.TOP, 8f) - .paddingDip(YogaEdge.BOTTOM, 20f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .backgroundRes(R.color.dark_hint_text) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.tag_sheet_choose_tag) - .marginDip(YogaEdge.BOTTOM, 12f)) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 20f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .backgroundRes(R.color.dark_hint_text) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.tag_sheet_choose_tag) + .marginDip(YogaEdge.BOTTOM, 12f)) getTagOptions().forEach { tagsComponent.child(TagItemLayout.create(componentContext).option(it)) } val addTag = LithoOptionsItem( - title = R.string.tag_sheet_new_tag_button, - subtitle = 0, - icon = R.drawable.icon_add_note, - listener = { CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { _, _ -> reset(activity, dialog) } }) + title = R.string.tag_sheet_new_tag_button, + subtitle = 0, + icon = R.drawable.icon_add_note, + listener = { CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { _, _ -> reset(activity, dialog) } }) tagsComponent.child(OptionItemLayout.create(componentContext).option(addTag).onClick { addTag.listener() }) component.child(tagsComponent) @@ -176,52 +187,52 @@ class HomeOptionsBottomSheet : LithoBottomSheet() { val activity = context as MainActivity val options = ArrayList() options.add(LithoLabelOptionsItem( - title = R.string.nav_home, - icon = R.drawable.ic_home_white_48dp, - listener = { - activity.onHomeClick() - dismiss() - } + title = R.string.nav_home, + icon = R.drawable.ic_home_white_48dp, + listener = { + activity.onHomeClick() + dismiss() + } )) options.add(LithoLabelOptionsItem( - title = R.string.nav_favourites, - icon = R.drawable.ic_favorite_white_48dp, - listener = { - activity.onFavouritesClick(); - dismiss(); - } + title = R.string.nav_favourites, + icon = R.drawable.ic_favorite_white_48dp, + listener = { + activity.onFavouritesClick(); + dismiss(); + } )) options.add(LithoLabelOptionsItem( - title = R.string.nav_archived, - icon = R.drawable.ic_archive_white_48dp, - listener = { - activity.onArchivedClick(); - dismiss(); - } + title = R.string.nav_archived, + icon = R.drawable.ic_archive_white_48dp, + listener = { + activity.onArchivedClick(); + dismiss(); + } )) options.add(LithoLabelOptionsItem( - title = R.string.nav_locked, - icon = R.drawable.ic_action_lock, - listener = { - activity.onLockedClick(); - dismiss(); - } + title = R.string.nav_locked, + icon = R.drawable.ic_action_lock, + listener = { + activity.onLockedClick(); + dismiss(); + } )) options.add(LithoLabelOptionsItem( - title = R.string.nav_trash, - icon = R.drawable.ic_delete_white_48dp, - listener = { - activity.onTrashClick(); - dismiss(); - } + title = R.string.nav_trash, + icon = R.drawable.ic_delete_white_48dp, + listener = { + activity.onTrashClick(); + dismiss(); + } )) options.add(LithoLabelOptionsItem( - title = R.string.nav_settings, - icon = R.drawable.ic_action_settings, - listener = { - SettingsOptionsBottomSheet.openSheet(activity) - dismiss(); - } + title = R.string.nav_settings, + icon = R.drawable.ic_action_settings, + listener = { + SettingsOptionsBottomSheet.openSheet(activity) + dismiss(); + } )) return options } @@ -231,18 +242,18 @@ class HomeOptionsBottomSheet : LithoBottomSheet() { val options = ArrayList() for (tag in CoreConfig.tagsDb.getAll()) { options.add(LithoTagOptionsItem( - tag = tag, - usages = CoreConfig.notesDb.getNoteCountByTag(tag.uuid), - listener = { - activity.config = SearchConfig(mode = HomeNavigationState.DEFAULT) - activity.openTag(tag) - dismiss() - }, - isEditable = true, - isSelected = false, - editListener = { - CreateOrEditTagBottomSheet.openSheet(activity, tag) { _, _ -> reset(activity, dialog) } - } + tag = tag, + usages = CoreConfig.notesDb.getNoteCountByTag(tag.uuid), + listener = { + activity.config = SearchConfig(mode = HomeNavigationState.DEFAULT) + activity.openTag(tag) + dismiss() + }, + isEditable = true, + isSelected = false, + editListener = { + CreateOrEditTagBottomSheet.openSheet(activity, tag) { _, _ -> reset(activity, dialog) } + } )) } options.sortByDescending { it.usages } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index 75286ae1..ebec9dbf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -8,7 +8,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -22,40 +21,43 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType class InstallProUpsellBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val options = listOf( - GridSectionOptionItem(R.drawable.ic_whats_new, R.string.install_pro_sheet_latest_updates, {}), - GridSectionOptionItem(R.drawable.ic_action_lock, R.string.install_pro_sheet_app_lock, {}), - GridSectionOptionItem(R.drawable.ic_action_day_mode, R.string.install_pro_sheet_app_themes, {}), - GridSectionOptionItem(R.drawable.ic_title_white_48dp, R.string.install_pro_sheet_font_size, {}), - GridSectionOptionItem(R.drawable.ic_note_white_48dp, R.string.install_pro_sheet_note_options, {}), - GridSectionOptionItem(R.drawable.ic_action_color, R.string.install_pro_sheet_viewer_bg, {}), - GridSectionOptionItem(R.drawable.icon_widget, R.string.install_pro_sheet_widget_options, {})) + GridSectionOptionItem(R.drawable.ic_whats_new, R.string.install_pro_sheet_latest_updates, {}), + GridSectionOptionItem(R.drawable.ic_action_lock, R.string.install_pro_sheet_app_lock, {}), + GridSectionOptionItem(R.drawable.ic_action_day_mode, R.string.install_pro_sheet_app_themes, {}), + GridSectionOptionItem(R.drawable.ic_title_white_48dp, R.string.install_pro_sheet_font_size, {}), + GridSectionOptionItem(R.drawable.ic_note_white_48dp, R.string.install_pro_sheet_note_options, {}), + GridSectionOptionItem(R.drawable.ic_action_color, R.string.install_pro_sheet_viewer_bg, {}), + GridSectionOptionItem(R.drawable.icon_widget, R.string.install_pro_sheet_widget_options, {})) val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.available_in_pro_only) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .textRes(R.string.why_install_pro) - .typeface(FONT_MONSERRAT) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(GridSectionView.create(componentContext) - .maxLines(3) - .numColumns(2) - .iconSizeRes(R.dimen.primary_round_icon_size) - .section(GridSectionItem(options = options)) - .showSeparator(false)) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.install_pro_app) - .onPrimaryClick { - IntentUtils.openAppPlayStore(activity, "com.bijoysingh.quicknote.pro") - dismiss() - } - .paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.available_in_pro_only) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .textRes(R.string.why_install_pro) + .typeface(FONT_MONSERRAT) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child( + GridSectionView.create(componentContext) + .maxLines(3) + .numColumns(2) + .iconSizeRes(R.dimen.primary_round_icon_size) + .section(GridSectionItem(options = options)) + .showSeparator(false)) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.install_pro_app) + .onPrimaryClick { + IntentUtils.openAppPlayStore(activity, "com.bijoysingh.quicknote.pro") + dismiss() + } + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index bf19b391..17aeae03 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -7,7 +7,6 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -26,39 +25,43 @@ class WhatsNewBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val options = listOf( - if (FlavorUtils.isOpenSource()) null else GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), - GridSectionOptionItem(R.drawable.icon_share_image, R.string.whats_new_sheet_photo_share, {}), - GridSectionOptionItem(R.drawable.ic_action_color, R.string.whats_new_sheet_note_color, {}), - GridSectionOptionItem(R.drawable.icon_languages, R.string.whats_new_sheet_more_languages, {}), - if (!OsVersionUtils.canAddLauncherShortcuts()) null else GridSectionOptionItem(R.drawable.icon_shortcut, R.string.whats_new_sheet_launcher_shortcuts, {}), - GridSectionOptionItem(R.drawable.ic_markdown_logo, R.string.whats_new_sheet_markdown_improvements, {}), - GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_ui_improvements, {})) + if (FlavorUtils.isOpenSource()) null else GridSectionOptionItem(R.drawable.gdrive_icon, R.string.whats_new_sheet_google_drive, {}), + GridSectionOptionItem(R.drawable.icon_share_image, R.string.whats_new_sheet_photo_share, {}), + GridSectionOptionItem(R.drawable.ic_action_color, R.string.whats_new_sheet_note_color, {}), + GridSectionOptionItem(R.drawable.icon_languages, R.string.whats_new_sheet_more_languages, {}), + if (!OsVersionUtils.canAddLauncherShortcuts()) null else GridSectionOptionItem( + R.drawable.icon_shortcut, R.string.whats_new_sheet_launcher_shortcuts, {}), + GridSectionOptionItem(R.drawable.ic_markdown_logo, R.string.whats_new_sheet_markdown_improvements, {}), + GridSectionOptionItem(R.drawable.icon_widget, R.string.whats_new_sheet_ui_improvements, {})) val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.whats_new_title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .textRes(R.string.whats_new_sheet_subtitle) - .typeface(FONT_MONSERRAT) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(GridSectionView.create(componentContext) - .maxLines(3) - .numColumns(2) - .iconSizeRes(R.dimen.ultra_large_round_icon_size) - .section(GridSectionItem(options = options.filterNotNull())) - .showSeparator(false)) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.import_export_layout_exporting_done) - .onPrimaryClick { - dismiss() - } - .paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.whats_new_title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .textRes(R.string.whats_new_sheet_subtitle) + .typeface(FONT_MONSERRAT) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child( + GridSectionView.create(componentContext) + .maxLines(3) + .numColumns(2) + .iconSizeRes(R.dimen.ultra_large_round_icon_size) + .section(GridSectionItem(options = options.filterNotNull())) + .showSeparator(false)) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + dismiss() + } + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 1a60d54d..26f7e7cb 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -6,7 +6,12 @@ import android.graphics.Color import android.graphics.drawable.Icon import android.net.Uri import android.text.Layout -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.LongClickEvent +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -18,7 +23,6 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT_MEDIUM @@ -44,59 +48,60 @@ import kotlinx.coroutines.launch @LayoutSpec object MainActivityBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig): Component { val activity = context.androidContext as MainActivity val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 4f) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 4f) row.child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.ic_apps_white_48dp) - .onClick { - openSheet(activity, HomeOptionsBottomSheet()) - }) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.ic_apps_white_48dp) + .onClick { + openSheet(activity, HomeOptionsBottomSheet()) + }) row.child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.icon_add_notebook) - .onClick { - CreateOrEditFolderBottomSheet.openSheet( - activity, - FolderBuilder().emptyFolder(sNoteDefaultColor), - { _, _ -> activity.setupData() }) - }) + .iconRes(R.drawable.icon_add_notebook) + .onClick { + CreateOrEditFolderBottomSheet.openSheet( + activity, + FolderBuilder().emptyFolder(sNoteDefaultColor), + { _, _ -> activity.setupData() }) + }) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.icon_add_list) - .onClick { - val intent = CreateNoteActivity.getNewChecklistNoteIntent( - activity, - activity.config.folders.firstOrNull()?.uuid ?: "") - activity.startActivity(intent) - }) + .iconRes(R.drawable.icon_add_list) + .onClick { + val intent = CreateNoteActivity.getNewChecklistNoteIntent( + activity, + activity.config.folders.firstOrNull()?.uuid ?: "") + activity.startActivity(intent) + }) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.icon_add_note) - .isLongClickEnabled(true) - .onLongClick { - if (!OsVersionUtils.canAddLauncherShortcuts()) { - return@onLongClick - } + .iconRes(R.drawable.icon_add_note) + .isLongClickEnabled(true) + .onLongClick { + if (!OsVersionUtils.canAddLauncherShortcuts()) { + return@onLongClick + } - val shortcut = ShortcutInfo.Builder(activity, "scarlet_notes___create_note") - .setShortLabel(activity.getString(R.string.shortcut_add_note)) - .setLongLabel(activity.getString(R.string.shortcut_add_note)) - .setIcon(Icon.createWithResource(activity, R.mipmap.create_launcher)) - .setIntent(Intent(Intent.ACTION_VIEW, Uri.parse("scarlet://create_note"))) - .build() - addShortcut(activity, shortcut) - } - .onClick { - val intent = CreateNoteActivity.getNewNoteIntent( - activity, - activity.config.folders.firstOrNull()?.uuid ?: "") - activity.startActivity(intent) - }) + val shortcut = ShortcutInfo.Builder(activity, "scarlet_notes___create_note") + .setShortLabel(activity.getString(R.string.shortcut_add_note)) + .setLongLabel(activity.getString(R.string.shortcut_add_note)) + .setIcon(Icon.createWithResource(activity, R.mipmap.create_launcher)) + .setIntent(Intent(Intent.ACTION_VIEW, Uri.parse("scarlet://create_note"))) + .build() + addShortcut(activity, shortcut) + } + .onClick { + val intent = CreateNoteActivity.getNewNoteIntent( + activity, + activity.config.folders.firstOrNull()?.uuid ?: "") + activity.startActivity(intent) + }) return bottomBarCard(context, row.build(), colorConfig).build() } } @@ -106,30 +111,31 @@ object MainActivityFolderBottomBarSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop folder: Folder): Component { val colorConfig = ToolbarColorConfig( - toolbarBackgroundColor = folder.color, - toolbarIconColor = when (ColorUtil.isLightColored(folder.color)) { - true -> context.getColor(R.color.dark_tertiary_text) - false -> context.getColor(R.color.light_secondary_text) - } + toolbarBackgroundColor = folder.color, + toolbarIconColor = when (ColorUtil.isLightColored(folder.color)) { + true -> context.getColor(R.color.dark_tertiary_text) + false -> context.getColor(R.color.light_secondary_text) + } ) val activity = context.androidContext as MainActivity val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 4f) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 4f) row.child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.ic_close_white_48dp) - .onClick { - GlobalScope.launch { - activity.config.folders.clear() - activity.unifiedSearch() - GlobalScope.launch(Dispatchers.Main) { - activity.notifyFolderChange() - } - } - }) - row.child(Text.create(context) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.ic_close_white_48dp) + .onClick { + GlobalScope.launch { + activity.config.folders.clear() + activity.unifiedSearch() + GlobalScope.launch(Dispatchers.Main) { + activity.notifyFolderChange() + } + } + }) + row.child( + Text.create(context) .typeface(FONT_MONSERRAT) .textAlignment(Layout.Alignment.ALIGN_CENTER) .flexGrow(1f) @@ -138,10 +144,10 @@ object MainActivityFolderBottomBarSpec { .textColor(colorConfig.toolbarIconColor) .clickHandler(MainActivityFolderBottomBar.onClickEvent(context))) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_more_options) - .isClickDisabled(true) - .clickHandler(MainActivityFolderBottomBar.onClickEvent(context)) - .onClick {}) + .iconRes(R.drawable.ic_more_options) + .isClickDisabled(true) + .clickHandler(MainActivityFolderBottomBar.onClickEvent(context)) + .onClick {}) return bottomBarCard(context, row.build(), colorConfig).build() } @@ -160,35 +166,37 @@ object MainActivityDisabledSyncSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { val colorConfig = ToolbarColorConfig( - toolbarBackgroundColor = context.getColor(R.color.material_blue_grey_800), - toolbarIconColor = context.getColor(R.color.light_secondary_text) + toolbarBackgroundColor = context.getColor(R.color.material_blue_grey_800), + toolbarIconColor = context.getColor(R.color.light_secondary_text) ) val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 4f) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 4f) row.child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.ic_info) - .onClick { - GlobalScope.launch { + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.ic_info) + .onClick { + GlobalScope.launch { - } - }) + } + }) row.child( - Column.create(context) - .flexGrow(1f) - .paddingDip(YogaEdge.ALL, 8f) - .child(Text.create(context) - .typeface(FONT_MONSERRAT_MEDIUM) - .textRes(R.string.firebase_no_sync_warning) - .textSizeRes(R.dimen.font_size_normal) - .textColor(colorConfig.toolbarIconColor)) - .child(Text.create(context) - .typeface(FONT_MONSERRAT) - .textRes(R.string.firebase_no_sync_warning_details) - .textSizeRes(R.dimen.font_size_small) - .textColor(colorConfig.toolbarIconColor))) + Column.create(context) + .flexGrow(1f) + .paddingDip(YogaEdge.ALL, 8f) + .child( + Text.create(context) + .typeface(FONT_MONSERRAT_MEDIUM) + .textRes(R.string.firebase_no_sync_warning) + .textSizeRes(R.dimen.font_size_normal) + .textColor(colorConfig.toolbarIconColor)) + .child( + Text.create(context) + .typeface(FONT_MONSERRAT) + .textRes(R.string.firebase_no_sync_warning_details) + .textSizeRes(R.dimen.font_size_small) + .textColor(colorConfig.toolbarIconColor))) row.clickHandler(MainActivityDisabledSync.onClickEvent(context)) return bottomBarCard(context, row.build(), colorConfig).build() } @@ -204,8 +212,8 @@ object MainActivitySyncingNowSpec { @OnCreateLayout fun onCreate(context: ComponentContext, @Prop isSyncHappening: Boolean): Component { val colorConfig = ToolbarColorConfig( - toolbarBackgroundColor = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), - toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) + toolbarBackgroundColor = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), + toolbarIconColor = sAppTheme.get(ThemeColorType.TOOLBAR_ICON) ) val syncText = when (isSyncHappening) { true -> R.string.home_syncing_top_layout @@ -213,39 +221,41 @@ object MainActivitySyncingNowSpec { } val syncIcon = when (isSyncHappening) { true -> Progress.create(context) - .widthDip(24f) - .alpha(0.8f) - .marginDip(YogaEdge.END, 8f) - .color(colorConfig.toolbarIconColor) + .widthDip(24f) + .alpha(0.8f) + .marginDip(YogaEdge.END, 8f) + .color(colorConfig.toolbarIconColor) false -> Image.create(context) - .heightDip(24f) - .widthDip(24f) - .marginDip(YogaEdge.END, 8f) - .alpha(0.8f) - .drawableRes(R.drawable.icon_folder_sync) + .heightDip(24f) + .widthDip(24f) + .marginDip(YogaEdge.END, 8f) + .alpha(0.8f) + .drawableRes(R.drawable.icon_folder_sync) } val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 8f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .alpha(0.8f) - .child(EmptySpec.create(context).flexGrow(1f)) - .child(Row.create(context) - .alignItems(YogaAlign.CENTER) - .alignContent(YogaAlign.CENTER) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .backgroundRes(R.drawable.pending_sync_capsule) - .clickHandler(MainActivitySyncingNow.onClickEvent(context)) - .longClickHandler(MainActivitySyncingNow.onLongClickEvent(context)) - .child(syncIcon) - .child(Text.create(context) - .typeface(FONT_MONSERRAT) - .textRes(syncText) - .textSizeRes(R.dimen.font_size_normal) - .textColorRes(R.color.light_secondary_text))) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 8f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .alpha(0.8f) + .child(EmptySpec.create(context).flexGrow(1f)) + .child( + Row.create(context) + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .backgroundRes(R.drawable.pending_sync_capsule) + .clickHandler(MainActivitySyncingNow.onClickEvent(context)) + .longClickHandler(MainActivitySyncingNow.onLongClickEvent(context)) + .child(syncIcon) + .child( + Text.create(context) + .typeface(FONT_MONSERRAT) + .textRes(syncText) + .textSizeRes(R.dimen.font_size_normal) + .textColorRes(R.color.light_secondary_text))) return row.build() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt index efde7ba3..f35140bf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/MarkdownExtensions.kt @@ -6,7 +6,8 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType fun String.toInternalFormats(): List { - return toInternalFormats(arrayOf( + return toInternalFormats( + arrayOf( MarkdownSegmentType.HEADING_1, MarkdownSegmentType.HEADING_2, MarkdownSegmentType.HEADING_3, diff --git a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt index 5501a243..ca0fc72d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/NoteExtensions.kt @@ -6,13 +6,21 @@ import android.support.v4.app.ActivityOptionsCompat import com.google.gson.Gson import com.maubis.markdown.Markdown import com.maubis.markdown.MarkdownConfig -import com.maubis.markdown.spannable.* +import com.maubis.markdown.spannable.MarkdownType +import com.maubis.markdown.spannable.bold +import com.maubis.markdown.spannable.font +import com.maubis.markdown.spannable.relativeSize +import com.maubis.markdown.spannable.strike import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType -import com.maubis.scarlet.base.core.note.* +import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.core.note.generateUUID +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.core.note.getTagUUIDs +import com.maubis.scarlet.base.core.note.isUnsaved import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity @@ -31,7 +39,6 @@ import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* import kotlin.collections.ArrayList - fun Note.log(): String { val log = HashMap() log["note"] = this @@ -71,20 +78,20 @@ internal fun markdownFormatForList(text: String): CharSequence { when (spanInfo.markdownType) { MarkdownType.HEADING_1 -> { spannable.relativeSize(1.2f, s, e) - .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) - .bold(s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) true } MarkdownType.HEADING_2 -> { spannable.relativeSize(1.1f, s, e) - .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) - .bold(s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) true } MarkdownType.HEADING_3 -> { spannable.relativeSize(1.0f, s, e) - .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) - .bold(s, e) + .font(MarkdownConfig.config.spanConfig.headingTypeface, s, e) + .bold(s, e) true } MarkdownType.CHECKLIST_CHECKED -> { @@ -96,7 +103,6 @@ internal fun markdownFormatForList(text: String): CharSequence { } } - fun Note.getTitleForSharing(): String { val formats = getFormats() if (formats.isEmpty()) { @@ -254,9 +260,9 @@ fun Note.edit(context: Context) { if (this.locked) { if (context is ThemedActivity) { openUnlockSheet( - activity = context, - onUnlockSuccess = { openEdit(context) }, - onUnlockFailure = { edit(context) }) + activity = context, + onUnlockSuccess = { openEdit(context) }, + onUnlockFailure = { edit(context) }) } return } @@ -287,12 +293,10 @@ fun Note.openEdit(context: Context) { context.startActivity(intent) } - fun Note.share(context: Context) { ApplicationBase.instance.noteActions(this).share(context) } - fun Note.hasImages(): Boolean { val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } return imageFormats.isNotEmpty() @@ -301,10 +305,10 @@ fun Note.hasImages(): Boolean { fun Note.shareImages(context: Context) { val imageFormats = getFormats().filter { it.formatType == FormatType.IMAGE } val bitmaps = imageFormats - .map { ApplicationBase.sAppImageStorage.getFile(uuid, it.text) } - .filter { it.exists() } - .map { BitmapHelper.loadFromFile(it) } - .filterNotNull() + .map { ApplicationBase.sAppImageStorage.getFile(uuid, it.text) } + .filter { it.exists() } + .map { BitmapHelper.loadFromFile(it) } + .filterNotNull() when { bitmaps.size == 1 -> BitmapHelper.send(context, bitmaps.first()) bitmaps.size > 1 -> BitmapHelper.send(context, bitmaps) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index 18ade7fb..a9330213 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -21,14 +21,23 @@ import com.maubis.scarlet.base.core.note.getNoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet import com.maubis.scarlet.base.main.sheets.openDeleteNotePermanentlySheet -import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.activity.INoteOptionSheetActivity +import com.maubis.scarlet.base.note.copy +import com.maubis.scarlet.base.note.edit import com.maubis.scarlet.base.note.folder.sheet.FolderChooserBottomSheet +import com.maubis.scarlet.base.note.getFullText +import com.maubis.scarlet.base.note.getTagString +import com.maubis.scarlet.base.note.getTitleForSharing +import com.maubis.scarlet.base.note.hasImages import com.maubis.scarlet.base.note.reminders.sheet.ReminderBottomSheet +import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.note.selection.activity.KEY_SELECT_EXTRA_MODE import com.maubis.scarlet.base.note.selection.activity.KEY_SELECT_EXTRA_NOTE_ID import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity +import com.maubis.scarlet.base.note.share +import com.maubis.scarlet.base.note.shareImages import com.maubis.scarlet.base.note.tag.sheet.TagChooserBottomSheet +import com.maubis.scarlet.base.note.viewDistractionFree import com.maubis.scarlet.base.notification.NotificationConfig import com.maubis.scarlet.base.notification.NotificationHandler import com.maubis.scarlet.base.security.sheets.openUnlockSheet @@ -46,7 +55,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch - class NoteOptionsBottomSheet() : GridBottomSheetBase() { var noteFn: () -> Note? = { null } @@ -66,14 +74,14 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { private fun setupGrid(dialog: Dialog, note: Note) { val gridLayoutIds = arrayOf( - R.id.quick_actions_properties, - R.id.note_properties, - R.id.grid_layout) + R.id.quick_actions_properties, + R.id.note_properties, + R.id.grid_layout) val gridOptionFunctions = arrayOf( - { noteForAction: Note -> getQuickActions(noteForAction) }, - { noteForAction: Note -> getNotePropertyOptions(noteForAction) }, - { noteForAction: Note -> getOptions(noteForAction) }) + { noteForAction: Note -> getQuickActions(noteForAction) }, + { noteForAction: Note -> getNotePropertyOptions(noteForAction) }, + { noteForAction: Note -> getOptions(noteForAction) }) gridOptionFunctions.forEachIndexed { index, function -> GlobalScope.launch(Dispatchers.Main) { val items = GlobalScope.async(Dispatchers.IO) { function(note) } @@ -137,7 +145,8 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { } val options = ArrayList() - options.add(OptionsItem( + options.add( + OptionsItem( title = R.string.restore_note, subtitle = R.string.tap_for_action_not_trash, icon = R.drawable.ic_restore, @@ -146,17 +155,18 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, visible = note.getNoteState() == NoteState.TRASH - )) + )) options.add(OptionsItem( - title = R.string.edit_note, - subtitle = R.string.tap_for_action_edit, - icon = R.drawable.ic_edit_white_48dp, - listener = View.OnClickListener { - note.edit(activity) - dismiss() - } + title = R.string.edit_note, + subtitle = R.string.tap_for_action_edit, + icon = R.drawable.ic_edit_white_48dp, + listener = View.OnClickListener { + note.edit(activity) + dismiss() + } )) - options.add(OptionsItem( + options.add( + OptionsItem( title = R.string.not_favourite_note, subtitle = R.string.tap_for_action_not_favourite, icon = R.drawable.ic_favorite_white_48dp, @@ -165,8 +175,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, visible = note.getNoteState() == NoteState.FAVOURITE - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.favourite_note, subtitle = R.string.tap_for_action_favourite, icon = R.drawable.ic_favorite_border_white_48dp, @@ -175,8 +186,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, visible = note.getNoteState() != NoteState.FAVOURITE - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.unarchive_note, subtitle = R.string.tap_for_action_not_archive, icon = R.drawable.ic_archive_white_48dp, @@ -185,8 +197,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, visible = note.getNoteState() == NoteState.ARCHIVED - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.archive_note, subtitle = R.string.tap_for_action_archive, icon = R.drawable.ic_archive_white_48dp, @@ -195,8 +208,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, visible = note.getNoteState() != NoteState.ARCHIVED - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.send_note, subtitle = R.string.tap_for_action_share, icon = R.drawable.ic_share_white_48dp, @@ -205,8 +219,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.copy_note, subtitle = R.string.tap_for_action_copy, icon = R.drawable.ic_content_copy_white_48dp, @@ -215,8 +230,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.delete_note_permanently, subtitle = R.string.tap_for_action_delete, icon = R.drawable.ic_delete_permanently, @@ -226,8 +242,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = note.getNoteState() == NoteState.TRASH, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.trash_note, subtitle = R.string.tap_for_action_trash, icon = R.drawable.ic_delete_white_48dp, @@ -237,7 +254,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = note.getNoteState() != NoteState.TRASH, invalid = activity.lockedContentIsHidden() && note.locked - )) + )) return options } @@ -249,38 +266,40 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { val options = ArrayList() options.add(OptionsItem( - title = R.string.choose_note_color, - subtitle = R.string.tap_for_action_color, - icon = R.drawable.ic_action_color, - listener = View.OnClickListener { - val config = ColorPickerDefaultController( - title = R.string.choose_note_color, - colors = listOf(activity.resources.getIntArray(R.array.bright_colors), activity.resources.getIntArray(R.array.bright_colors_accent)), - selectedColor = note.color, - onColorSelected = { color -> - note.color = color - activity.updateNote(note) - } - ) - com.maubis.scarlet.base.support.sheets.openSheet(activity, ColorPickerBottomSheet().apply { this.config = config }) - dismiss() - } + title = R.string.choose_note_color, + subtitle = R.string.tap_for_action_color, + icon = R.drawable.ic_action_color, + listener = View.OnClickListener { + val config = ColorPickerDefaultController( + title = R.string.choose_note_color, + colors = listOf( + activity.resources.getIntArray(R.array.bright_colors), activity.resources.getIntArray(R.array.bright_colors_accent)), + selectedColor = note.color, + onColorSelected = { color -> + note.color = color + activity.updateNote(note) + } + ) + com.maubis.scarlet.base.support.sheets.openSheet(activity, ColorPickerBottomSheet().apply { this.config = config }) + dismiss() + } )) options.add(OptionsItem( - title = if (note.folder.isBlank()) R.string.folder_option_add_to_notebook else R.string.folder_option_change_notebook, - subtitle = R.string.folder_option_add_to_notebook, - icon = R.drawable.ic_folder, - listener = View.OnClickListener { - com.maubis.scarlet.base.support.sheets.openSheet(activity, FolderChooserBottomSheet().apply { - this.note = note - this.dismissListener = { - activity.notifyResetOrDismiss() - } - }) - dismiss() - } + title = if (note.folder.isBlank()) R.string.folder_option_add_to_notebook else R.string.folder_option_change_notebook, + subtitle = R.string.folder_option_add_to_notebook, + icon = R.drawable.ic_folder, + listener = View.OnClickListener { + com.maubis.scarlet.base.support.sheets.openSheet(activity, FolderChooserBottomSheet().apply { + this.note = note + this.dismissListener = { + activity.notifyResetOrDismiss() + } + }) + dismiss() + } )) - options.add(OptionsItem( + options.add( + OptionsItem( title = R.string.lock_note, subtitle = R.string.lock_note, icon = R.drawable.ic_action_lock, @@ -290,23 +309,24 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, visible = !note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.unlock_note, subtitle = R.string.unlock_note, icon = R.drawable.ic_action_unlock, listener = View.OnClickListener { openUnlockSheet( - activity = activity, - onUnlockSuccess = { - note.locked = false - activity.updateNote(note) - dismiss() - }, - onUnlockFailure = { }) + activity = activity, + onUnlockSuccess = { + note.locked = false + activity.updateNote(note) + dismiss() + }, + onUnlockFailure = { }) }, visible = note.locked - )) + )) return options } @@ -318,16 +338,17 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { val options = ArrayList() options.add(OptionsItem( - title = if (note.pinned) R.string.unpin_note else R.string.pin_note, - subtitle = if (note.pinned) R.string.unpin_note else R.string.pin_note, - icon = R.drawable.ic_pin, - listener = View.OnClickListener { - note.pinned = !note.pinned - activity.updateNote(note) - dismiss() - } + title = if (note.pinned) R.string.unpin_note else R.string.pin_note, + subtitle = if (note.pinned) R.string.unpin_note else R.string.pin_note, + icon = R.drawable.ic_pin, + listener = View.OnClickListener { + note.pinned = !note.pinned + activity.updateNote(note) + dismiss() + } )) - options.add(OptionsItem( + options.add( + OptionsItem( title = R.string.share_images, subtitle = R.string.share_images, icon = R.drawable.icon_share_image, @@ -337,8 +358,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = note.hasImages(), invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.open_in_notification, subtitle = R.string.open_in_notification, icon = R.drawable.ic_action_notification, @@ -348,8 +370,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.delete_note_permanently, subtitle = R.string.delete_note_permanently, icon = R.drawable.ic_delete_permanently, @@ -359,9 +382,10 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = note.getNoteState() !== NoteState.TRASH, invalid = activity.lockedContentIsHidden() && note.locked - )) + )) - options.add(OptionsItem( + options.add( + OptionsItem( title = R.string.pin_to_launcher, subtitle = R.string.pin_to_launcher, icon = R.drawable.icon_shortcut, @@ -373,12 +397,14 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { } val shortcut = ShortcutInfo.Builder(activity, "scarlet_notes___${note.uuid}") - .setShortLabel(title) - .setLongLabel(title) - .setIcon(Icon.createWithResource(activity, R.mipmap.open_note_launcher)) - .setIntent(Intent(Intent.ACTION_VIEW, - Uri.parse("scarlet://open_note?uuid=" + note.uuid))) - .build() + .setShortLabel(title) + .setLongLabel(title) + .setIcon(Icon.createWithResource(activity, R.mipmap.open_note_launcher)) + .setIntent( + Intent( + Intent.ACTION_VIEW, + Uri.parse("scarlet://open_note?uuid=" + note.uuid))) + .build() addShortcut(activity, shortcut) return@OnClickListener } @@ -386,8 +412,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = OsVersionUtils.canAddLauncherShortcuts(), invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.reminder, subtitle = R.string.reminder, icon = R.drawable.ic_action_reminder_icon, @@ -396,8 +423,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.duplicate, subtitle = R.string.duplicate, icon = R.drawable.ic_duplicate, @@ -410,8 +438,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.voice_action_title, subtitle = R.string.voice_action_title, icon = R.drawable.ic_action_speak_aloud, @@ -420,8 +449,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.view_distraction_free, subtitle = R.string.view_distraction_free, icon = R.drawable.ic_action_distraction_free, @@ -433,8 +463,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { openSheet(activity, InstallProUpsellBottomSheet()) }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.open_in_popup, subtitle = R.string.tap_for_action_popup, icon = R.drawable.ic_bubble_chart_white_48dp, @@ -443,8 +474,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { dismiss() }, invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.backup_note_enable, subtitle = R.string.backup_note_enable, icon = R.drawable.ic_action_backup, @@ -455,8 +487,9 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = note.disableBackup && FlavorUtils.isPlayStore(), invalid = activity.lockedContentIsHidden() && note.locked - )) - options.add(OptionsItem( + )) + options.add( + OptionsItem( title = R.string.backup_note_disable, subtitle = R.string.backup_note_disable, icon = R.drawable.ic_action_backup_no, @@ -467,7 +500,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { }, visible = !note.disableBackup && FlavorUtils.isPlayStore(), invalid = activity.lockedContentIsHidden() && note.locked - )) + )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt index fa74c846..bbcefa44 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/TextToSpeechBottomSheet.kt @@ -6,14 +6,12 @@ import android.widget.ImageView import android.widget.TextView import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.getFullText import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment -import com.maubis.scarlet.base.support.utils.OsVersionUtils import com.maubis.scarlet.base.support.utils.removeMarkdownHeaders fun Note.getTextToSpeechText(): String { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt index a1dba26f..69f7f535 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/CreateNoteActivity.kt @@ -55,7 +55,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { setTouchListener() startHandler() } - + override fun onCreationFinished() { super.onCreationFinished() history.add(NoteBuilder().copy(note!!)) @@ -94,8 +94,8 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { addDefaultItem() } !formats[0].text.startsWith("# ") && - formats[0].formatType !== FormatType.HEADING - && formats[0].formatType !== FormatType.IMAGE -> { + formats[0].formatType !== FormatType.HEADING + && formats[0].formatType !== FormatType.IMAGE -> { addEmptyItem(0, FormatType.HEADING) } } @@ -114,19 +114,20 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { lithoTopToolbar.removeAllViews() val componentContext = ComponentContext(this) lithoTopToolbar.addView( - LithoView.create(componentContext, - NoteCreationTopBar.create(componentContext).build())) + LithoView.create( + componentContext, + NoteCreationTopBar.create(componentContext).build())) } override fun setBottomToolbar() { val componentContext = ComponentContext(this) lithoBottomToolbar.removeAllViews() lithoBottomToolbar.addView( - LithoView.create( - componentContext, - NoteCreationBottomBar.create(componentContext) - .colorConfig(ToolbarColorConfig(colorConfig.toolbarBackgroundColor, colorConfig.toolbarIconColor)) - .build())) + LithoView.create( + componentContext, + NoteCreationBottomBar.create(componentContext) + .colorConfig(ToolbarColorConfig(colorConfig.toolbarBackgroundColor, colorConfig.toolbarIconColor)) + .build())) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -194,7 +195,7 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { // Ignore update if nothing changed. It allows for one undo per few seconds when { - !historyModified && currentNote.isEqual(vLastNoteInstance) -> return + !historyModified && currentNote.isEqual(vLastNoteInstance) -> return !historyModified -> addNoteToHistory(NoteBuilder().copy(currentNote)) else -> historyModified = false } @@ -220,7 +221,6 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { } } - private fun startHandler() { val handler = Handler() handler.postDelayed(object : Runnable { @@ -327,12 +327,12 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { fun onColorChangeClick() { val config = ColorPickerDefaultController( - title = R.string.choose_note_color, - colors = listOf(resources.getIntArray(R.array.bright_colors), resources.getIntArray(R.array.bright_colors_accent)), - selectedColor = note!!.color, - onColorSelected = { color -> - setNoteColor(color) - } + title = R.string.choose_note_color, + colors = listOf(resources.getIntArray(R.array.bright_colors), resources.getIntArray(R.array.bright_colors_accent)), + selectedColor = note!!.color, + onColorSelected = { color -> + setNoteColor(color) + } ) com.maubis.scarlet.base.support.sheets.openSheet(this, ColorPickerBottomSheet().apply { this.config = config }) } @@ -410,8 +410,8 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { } val isCheckList = - (format.formatType === FormatType.CHECKLIST_UNCHECKED - || format.formatType === FormatType.CHECKLIST_CHECKED) + (format.formatType === FormatType.CHECKLIST_UNCHECKED + || format.formatType === FormatType.CHECKLIST_CHECKED) val newPosition = position + 1 when { isCheckList -> addEmptyItemAtFocused(FormatBuilder().getNextFormatType(FormatType.CHECKLIST_UNCHECKED)) @@ -424,16 +424,16 @@ open class CreateNoteActivity : ViewAdvancedNoteActivity() { private const val INTENT_KEY_FOLDER = "key_folder" fun getNewNoteIntent( - context: Context, - folder: String = ""): Intent { + context: Context, + folder: String = ""): Intent { val intent = Intent(context, CreateNoteActivity::class.java) intent.putExtra(INTENT_KEY_FOLDER, folder) return intent } fun getNewChecklistNoteIntent( - context: Context, - folder: String = ""): Intent { + context: Context, + folder: String = ""): Intent { val intent = Intent(context, CreateListNoteActivity::class.java) intent.putExtra(INTENT_KEY_FOLDER, folder) return intent diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt index e5d7beba..03bf391e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ShareToScarletRouterActivity.kt @@ -46,7 +46,7 @@ class ShareToScarletRouterActivity : AppCompatActivity() { private fun handleSendText(intent: Intent): Note? { val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "" val sharedSubject = intent.getStringExtra(Intent.EXTRA_SUBJECT) - ?: intent.getStringExtra(Intent.EXTRA_TITLE) ?: "" + ?: intent.getStringExtra(Intent.EXTRA_TITLE) ?: "" val sharedImages = when { intent.action == Intent.ACTION_SEND -> handleSendImage(intent) intent.action == Intent.ACTION_SEND_MULTIPLE -> handleSendMultipleImages(intent) @@ -118,7 +118,8 @@ class ShareToScarletRouterActivity : AppCompatActivity() { private fun isCallerKeep(): Boolean { return try { when { - OsVersionUtils.canExtractReferrer() && (referrer?.toString() ?: "").contains(KEEP_PACKAGE) -> true + OsVersionUtils.canExtractReferrer() && (referrer?.toString() + ?: "").contains(KEEP_PACKAGE) -> true callingPackage?.contains(KEEP_PACKAGE) ?: false -> true (intent?.`package` ?: "").contains(KEEP_PACKAGE) -> true else -> false diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt index 9a212108..7c0066d7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/activity/ViewNoteActivity.kt @@ -20,9 +20,12 @@ import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatBuilder import com.maubis.scarlet.base.core.format.FormatType import com.maubis.scarlet.base.core.format.sectionPreservingSort -import com.maubis.scarlet.base.core.note.* +import com.maubis.scarlet.base.core.note.NoteBuilder +import com.maubis.scarlet.base.core.note.NoteState +import com.maubis.scarlet.base.core.note.generateUUID +import com.maubis.scarlet.base.core.note.getFormats +import com.maubis.scarlet.base.core.note.isUnsaved import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.* import com.maubis.scarlet.base.note.actions.NoteOptionsBottomSheet import com.maubis.scarlet.base.note.activity.INoteOptionSheetActivity import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor @@ -33,13 +36,24 @@ import com.maubis.scarlet.base.note.formats.IFormatRecyclerViewActivity import com.maubis.scarlet.base.note.formats.getFormatControllerItems import com.maubis.scarlet.base.note.formats.recycler.KEY_EDITABLE import com.maubis.scarlet.base.note.formats.recycler.KEY_NOTE_COLOR +import com.maubis.scarlet.base.note.getSmartFormats +import com.maubis.scarlet.base.note.getTagString +import com.maubis.scarlet.base.note.mark +import com.maubis.scarlet.base.note.openEdit +import com.maubis.scarlet.base.note.save +import com.maubis.scarlet.base.note.saveWithoutSync +import com.maubis.scarlet.base.note.softDelete import com.maubis.scarlet.base.settings.sheet.STORE_KEY_TEXT_SIZE import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet.Companion.KEY_MARKDOWN_ENABLED import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet.Companion.useNoteColorAsBackground import com.maubis.scarlet.base.settings.sheet.sEditorTextSize import com.maubis.scarlet.base.support.specs.ToolbarColorConfig -import com.maubis.scarlet.base.support.ui.* +import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.support.ui.ColorUtil.darkerColor +import com.maubis.scarlet.base.support.ui.KEY_NIGHT_THEME +import com.maubis.scarlet.base.support.ui.SecuredActivity +import com.maubis.scarlet.base.support.ui.Theme +import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.utils.bind import com.maubis.scarlet.base.widget.getPendingIntentWithStack import kotlinx.android.synthetic.main.activity_advanced_note.* @@ -49,16 +63,14 @@ import kotlinx.coroutines.launch import java.util.* import java.util.concurrent.atomic.AtomicBoolean - const val INTENT_KEY_NOTE_ID = "NOTE_ID" const val INTENT_KEY_DISTRACTION_FREE = "DISTRACTION_FREE" - data class NoteViewColorConfig( - var backgroundColor: Int = Color.BLACK, - var toolbarBackgroundColor: Int = Color.BLACK, - var toolbarIconColor: Int = Color.BLACK, - var statusBarColor: Int = Color.BLACK) + var backgroundColor: Int = Color.BLACK, + var toolbarBackgroundColor: Int = Color.BLACK, + var toolbarIconColor: Int = Color.BLACK, + var statusBarColor: Int = Color.BLACK) open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivity, IFormatRecyclerViewActivity { @@ -141,11 +153,11 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit private fun startDistractionFreeMode() { val uiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) window.decorView.systemUiVisibility = uiVisibility } @@ -209,9 +221,9 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit private fun setRecyclerView() { adapter = FormatAdapter(this) formatsView = RecyclerViewBuilder(this) - .setAdapter(adapter) - .setView(this, R.id.advanced_note_recycler) - .build() + .setAdapter(adapter) + .setView(this, R.id.advanced_note_recycler) + .build() } open fun setFormat(format: Format) { @@ -295,18 +307,20 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit lithoBottomToolbar.removeAllViews() val componentContext = ComponentContext(this) lithoBottomToolbar.addView( - LithoView.create(componentContext, - NoteViewBottomBar.create(componentContext) - .colorConfig(ToolbarColorConfig(colorConfig.toolbarBackgroundColor, colorConfig.toolbarIconColor)) - .build())) + LithoView.create( + componentContext, + NoteViewBottomBar.create(componentContext) + .colorConfig(ToolbarColorConfig(colorConfig.toolbarBackgroundColor, colorConfig.toolbarIconColor)) + .build())) } protected open fun setTopToolbar() { lithoTopToolbar.removeAllViews() val componentContext = ComponentContext(this) lithoTopToolbar.addView( - LithoView.create(componentContext, - NoteViewTopBar.create(componentContext).build())) + LithoView.create( + componentContext, + NoteViewTopBar.create(componentContext).build())) } protected open fun setNoteColor(color: Int) { @@ -407,7 +421,6 @@ open class ViewAdvancedNoteActivity : SecuredActivity(), INoteOptionSheetActivit * End : INoteOptionSheetActivity */ - /** * Start : IFormatRecyclerView Functions */ diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt index 9f554788..5bdcc0d4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/EditorOptionsBottomSheet.kt @@ -50,76 +50,76 @@ class EditorOptionsBottomSheet : LithoOptionBottomSheet() { val items = ArrayList() val activity = context as MainActivity items.add(LithoOptionsItem( - title = R.string.note_option_default_color, - subtitle = R.string.note_option_default_color_subtitle, - icon = R.drawable.ic_action_color, - listener = { - val config = ColorPickerDefaultController( - title = R.string.note_option_default_color, - colors = listOf( - activity.resources.getIntArray(R.array.bright_colors), - activity.resources.getIntArray(R.array.bright_colors_accent)), - selectedColor = sNoteDefaultColor, - onColorSelected = { sNoteDefaultColor = it } - ) - openSheet(activity, ColorPickerBottomSheet().apply { this.config = config }) - dismiss() - } + title = R.string.note_option_default_color, + subtitle = R.string.note_option_default_color_subtitle, + icon = R.drawable.ic_action_color, + listener = { + val config = ColorPickerDefaultController( + title = R.string.note_option_default_color, + colors = listOf( + activity.resources.getIntArray(R.array.bright_colors), + activity.resources.getIntArray(R.array.bright_colors_accent)), + selectedColor = sNoteDefaultColor, + onColorSelected = { sNoteDefaultColor = it } + ) + openSheet(activity, ColorPickerBottomSheet().apply { this.config = config }) + dismiss() + } )) items.add(LithoOptionsItem( - title = R.string.markdown_sheet_markdown_support, - subtitle = R.string.markdown_sheet_markdown_support_subtitle, - icon = R.drawable.ic_markdown_logo, - selected = sEditorMarkdownEnabled, - isSelectable = true, - listener = { - sEditorMarkdownEnabled = !sEditorMarkdownEnabled - reset(componentContext.androidContext, dialog) - } + title = R.string.markdown_sheet_markdown_support, + subtitle = R.string.markdown_sheet_markdown_support_subtitle, + icon = R.drawable.ic_markdown_logo, + selected = sEditorMarkdownEnabled, + isSelectable = true, + listener = { + sEditorMarkdownEnabled = !sEditorMarkdownEnabled + reset(componentContext.androidContext, dialog) + } )) items.add(LithoOptionsItem( - title = R.string.editor_option_enable_live_markdown, - subtitle = R.string.editor_option_enable_live_markdown_description, - icon = R.drawable.icon_realtime_markdown, - selected = sEditorLiveMarkdown, - isSelectable = true, - listener = { - sEditorLiveMarkdown = !sEditorLiveMarkdown - reset(componentContext.androidContext, dialog) - } + title = R.string.editor_option_enable_live_markdown, + subtitle = R.string.editor_option_enable_live_markdown_description, + icon = R.drawable.icon_realtime_markdown, + selected = sEditorLiveMarkdown, + isSelectable = true, + listener = { + sEditorLiveMarkdown = !sEditorLiveMarkdown + reset(componentContext.androidContext, dialog) + } )) items.add(LithoOptionsItem( - title = R.string.editor_option_enable_markdown_mode_default, - subtitle = R.string.editor_option_enable_markdown_mode_default_details, - icon = R.drawable.ic_formats_logo, - selected = sEditorMarkdownDefault, - isSelectable = true, - listener = { - sEditorMarkdownDefault = !sEditorMarkdownDefault - reset(componentContext.androidContext, dialog) - } + title = R.string.editor_option_enable_markdown_mode_default, + subtitle = R.string.editor_option_enable_markdown_mode_default_details, + icon = R.drawable.ic_formats_logo, + selected = sEditorMarkdownDefault, + isSelectable = true, + listener = { + sEditorMarkdownDefault = !sEditorMarkdownDefault + reset(componentContext.androidContext, dialog) + } )) items.add(LithoOptionsItem( - title = R.string.editor_option_move_checked_items, - subtitle = R.string.editor_option_move_checked_items_description, - icon = R.drawable.ic_check_box_white_24dp, - selected = sEditorMoveChecked, - isSelectable = true, - listener = { - sEditorMoveChecked = !sEditorMoveChecked - reset(componentContext.androidContext, dialog) - } + title = R.string.editor_option_move_checked_items, + subtitle = R.string.editor_option_move_checked_items_description, + icon = R.drawable.ic_check_box_white_24dp, + selected = sEditorMoveChecked, + isSelectable = true, + listener = { + sEditorMoveChecked = !sEditorMoveChecked + reset(componentContext.androidContext, dialog) + } )) items.add(LithoOptionsItem( - title = R.string.editor_option_enable_move_handle, - subtitle = R.string.editor_option_enable_move_handle_description, - icon = R.drawable.icon_drag_indicator, - selected = sEditorMoveHandles, - isSelectable = true, - listener = { - sEditorMoveHandles = !sEditorMoveHandles - reset(componentContext.androidContext, dialog) - } + title = R.string.editor_option_enable_move_handle, + subtitle = R.string.editor_option_enable_move_handle_description, + icon = R.drawable.icon_drag_indicator, + selected = sEditorMoveHandles, + isSelectable = true, + listener = { + sEditorMoveHandles = !sEditorMoveHandles + reset(componentContext.androidContext, dialog) + } )) return items } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt index 81cb611e..ac210e28 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/FormatActionBottomSheet.kt @@ -33,19 +33,21 @@ class FormatActionBottomSheet : GridOptionBottomSheet() { } val format: Format = this.format!! - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.import_export_layout_exporting_share, icon = R.drawable.ic_share_white_48dp, listener = { IntentUtils.ShareBuilder(activity) - .setChooserText(activity.getString(R.string.share_using)) - .setText(format.text) - .share() + .setChooserText(activity.getString(R.string.share_using)) + .setText(format.text) + .share() dismiss() }, visible = !arrayOf(FormatType.IMAGE, FormatType.SEPARATOR).contains(format.formatType) - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.format_action_copy, icon = R.drawable.ic_content_copy_white_48dp, listener = { @@ -53,33 +55,35 @@ class FormatActionBottomSheet : GridOptionBottomSheet() { dismiss() }, visible = !arrayOf(FormatType.IMAGE, FormatType.SEPARATOR).contains(format.formatType) - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.format_action_camera, icon = R.drawable.ic_image_camera, listener = { EasyImage.openCamera(activity, format.uid) }, visible = format.formatType === FormatType.IMAGE - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.format_action_gallery, icon = R.drawable.ic_image_gallery, listener = { EasyImage.openGallery(activity, format.uid) }, visible = format.formatType === FormatType.IMAGE - )) + )) options.add(GridSectionOptionItem( - label = R.string.delete_sheet_delete_trash_yes, - icon = R.drawable.ic_delete_white_48dp, - listener = { - activity.deleteFormat(format) - if (format.formatType === FormatType.IMAGE && !format.text.isBlank()) { - deleteIfExist(sAppImageStorage.getFile(noteUUID, format)) - } - dismiss() + label = R.string.delete_sheet_delete_trash_yes, + icon = R.drawable.ic_delete_white_48dp, + listener = { + activity.deleteFormat(format) + if (format.formatType === FormatType.IMAGE && !format.text.isBlank()) { + deleteIfExist(sAppImageStorage.getFile(noteUUID, format)) } + dismiss() + } )) sections.add(GridSectionItem(options = options)) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt index b6b76cc8..6c39435f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt @@ -8,7 +8,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -17,19 +16,22 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType class MarkdownHelpBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val column = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child(getLithoBottomSheetTitle(componentContext).textRes(R.string.markdown_help_sheet_title)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child(getLithoBottomSheetTitle(componentContext).textRes(R.string.markdown_help_sheet_title)) - val examples = arrayOf("# Heading", "## Sub Heading", "```\nblock of code\n```", "> quoted text", "**bold**", "*italics*", "_underline_", "~~strike through~~", "`piece of code`") + val examples = arrayOf( + "# Heading", "## Sub Heading", "```\nblock of code\n```", "> quoted text", "**bold**", "*italics*", "_underline_", "~~strike through~~", + "`piece of code`") examples.forEach { column - .child(Text.create(componentContext) - .text(Markdown.render(it)) - .textSizeRes(R.dimen.font_size_normal) - .marginDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 4f) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) + .child( + Text.create(componentContext) + .text(Markdown.render(it)) + .textSizeRes(R.dimen.font_size_normal) + .marginDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 4f) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) } return column.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewBottomBarSpec.kt index 30b09346..72db70e8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewBottomBarSpec.kt @@ -1,8 +1,20 @@ package com.maubis.scarlet.base.note.creation.specs import android.graphics.Color -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.EventHandler +import com.facebook.litho.Row +import com.facebook.litho.StateValue +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateInitialState +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.OnUpdateState +import com.facebook.litho.annotations.Param +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.State import com.facebook.litho.widget.EmptyComponent import com.facebook.litho.widget.HorizontalScroll import com.facebook.yoga.YogaAlign @@ -35,62 +47,63 @@ object NoteCreationBottomBarSpec { @OnCreateInitialState fun onCreateInitialState( - context: ComponentContext, - state: StateValue) { + context: ComponentContext, + state: StateValue) { state.set(if (sEditorMarkdownDefault) NoteCreateBottomBarType.DEFAULT_MARKDOWNS else NoteCreateBottomBarType.DEFAULT_SEGMENTS) } @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig, - @State state: NoteCreateBottomBarType): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig, + @State state: NoteCreateBottomBarType): Component { val row = Row.create(context) - .widthPercent(100f) - .paddingDip(YogaEdge.HORIZONTAL, 4f) - .alignItems(YogaAlign.CENTER) + .widthPercent(100f) + .paddingDip(YogaEdge.HORIZONTAL, 4f) + .alignItems(YogaAlign.CENTER) val content = when (state) { NoteCreateBottomBarType.DEFAULT_SEGMENTS -> NoteCreationSegmentsBottomBar.create(context) - .colorConfig(colorConfig) - .flexGrow(1f) - .toggleButtonClick(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.ALL_SEGMENTS)) - NoteCreateBottomBarType.DEFAULT_MARKDOWNS -> NoteCreationMarkdownsBottomBar.create(context) .colorConfig(colorConfig) .flexGrow(1f) - .toggleButtonClick(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.ALL_MARKDOWNS)) + .toggleButtonClick(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.ALL_SEGMENTS)) + NoteCreateBottomBarType.DEFAULT_MARKDOWNS -> NoteCreationMarkdownsBottomBar.create(context) + .colorConfig(colorConfig) + .flexGrow(1f) + .toggleButtonClick(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.ALL_MARKDOWNS)) NoteCreateBottomBarType.ALL_SEGMENTS -> HorizontalScroll.create(context) - .flexGrow(1f) - .contentProps(NoteCreationAllSegmentsBottomBar.create(context).colorConfig(colorConfig)) + .flexGrow(1f) + .contentProps(NoteCreationAllSegmentsBottomBar.create(context).colorConfig(colorConfig)) NoteCreateBottomBarType.ALL_MARKDOWNS -> HorizontalScroll.create(context) - .flexGrow(1f) - .contentProps(NoteCreationAllMarkdownsBottomBar.create(context).colorConfig(colorConfig)) + .flexGrow(1f) + .contentProps(NoteCreationAllMarkdownsBottomBar.create(context).colorConfig(colorConfig)) NoteCreateBottomBarType.OPTIONS -> NoteCreationOptionsBottomBar.create(context) - .colorConfig(colorConfig) - .flexGrow(1f) + .colorConfig(colorConfig) + .flexGrow(1f) } row.child(content) val extraRoundIcon = bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .onClick { } - .isClickDisabled(true) + .bgColor(Color.TRANSPARENT) + .onClick { } + .isClickDisabled(true) val icon = when (state) { NoteCreateBottomBarType.DEFAULT_SEGMENTS -> extraRoundIcon - .iconRes(R.drawable.ic_markdown_logo) - .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_MARKDOWNS)) + .iconRes(R.drawable.ic_markdown_logo) + .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_MARKDOWNS)) NoteCreateBottomBarType.DEFAULT_MARKDOWNS -> extraRoundIcon - .iconRes(R.drawable.ic_formats_logo) - .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_SEGMENTS)) + .iconRes(R.drawable.ic_formats_logo) + .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_SEGMENTS)) NoteCreateBottomBarType.ALL_SEGMENTS -> extraRoundIcon - .marginDip(YogaEdge.HORIZONTAL, 4f) - .iconRes(R.drawable.ic_close_white_48dp) - .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_SEGMENTS)) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .iconRes(R.drawable.ic_close_white_48dp) + .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_SEGMENTS)) NoteCreateBottomBarType.ALL_MARKDOWNS -> extraRoundIcon - .marginDip(YogaEdge.HORIZONTAL, 4f) - .iconRes(R.drawable.ic_close_white_48dp) - .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_MARKDOWNS)) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .iconRes(R.drawable.ic_close_white_48dp) + .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.DEFAULT_MARKDOWNS)) NoteCreateBottomBarType.OPTIONS -> EmptyComponent.create(context) } row.child(icon) @@ -98,11 +111,11 @@ object NoteCreationBottomBarSpec { val moreIcon = when (state) { NoteCreateBottomBarType.DEFAULT_MARKDOWNS, NoteCreateBottomBarType.DEFAULT_SEGMENTS, NoteCreateBottomBarType.OPTIONS -> bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_more_options) - .bgColor(Color.TRANSPARENT) - .onClick { } - .isClickDisabled(true) - .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.OPTIONS)) + .iconRes(R.drawable.ic_more_options) + .bgColor(Color.TRANSPARENT) + .onClick { } + .isClickDisabled(true) + .clickHandler(NoteCreationBottomBar.onStateChangeClick(context, NoteCreateBottomBarType.OPTIONS)) else -> EmptyComponent.create(context) } row.child(moreIcon) @@ -110,9 +123,10 @@ object NoteCreationBottomBarSpec { } @OnEvent(ClickEvent::class) - fun onStateChangeClick(context: ComponentContext, - @State state: NoteCreateBottomBarType, - @Param nextState: NoteCreateBottomBarType) { + fun onStateChangeClick( + context: ComponentContext, + @State state: NoteCreateBottomBarType, + @Param nextState: NoteCreateBottomBarType) { if (state == NoteCreateBottomBarType.OPTIONS && nextState == NoteCreateBottomBarType.OPTIONS) { NoteCreationBottomBar.onStateChange(context, NoteCreateBottomBarType.DEFAULT_SEGMENTS) return @@ -137,209 +151,214 @@ object NoteCreationBottomBarSpec { @LayoutSpec object NoteCreationOptionsBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig): Component { val activity = context.androidContext as CreateNoteActivity return Row.create(context) - .alignItems(YogaAlign.CENTER) - .child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.icon_markdown_help) - .onClick { openSheet(activity, MarkdownHelpBottomSheet()) }) - .child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_undo_history) - .onClick { activity.onHistoryClick(true) }) - .child(bottomBarRoundIcon(context, colorConfig) - .bgColor(activity.note().color) - .bgAlpha(255) - .iconRes(R.drawable.ic_empty) - .onClick { activity.onColorChangeClick() } - .showBorder(true) - .iconMarginHorizontalRes(R.dimen.toolbar_round_small_icon_margin_horizontal) - .iconSizeRes(R.dimen.toolbar_round_small_icon_size)) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_redo_history) - .onClick { activity.onHistoryClick(false) }) - .child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) - .build() + .alignItems(YogaAlign.CENTER) + .child(bottomBarRoundIcon(context, colorConfig) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.icon_markdown_help) + .onClick { openSheet(activity, MarkdownHelpBottomSheet()) }) + .child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_undo_history) + .onClick { activity.onHistoryClick(true) }) + .child(bottomBarRoundIcon(context, colorConfig) + .bgColor(activity.note().color) + .bgAlpha(255) + .iconRes(R.drawable.ic_empty) + .onClick { activity.onColorChangeClick() } + .showBorder(true) + .iconMarginHorizontalRes(R.dimen.toolbar_round_small_icon_margin_horizontal) + .iconSizeRes(R.dimen.toolbar_round_small_icon_size)) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_redo_history) + .onClick { activity.onHistoryClick(false) }) + .child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) + .build() } } @LayoutSpec object NoteCreationSegmentsBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig, - @Prop toggleButtonClick: EventHandler): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig, + @Prop toggleButtonClick: EventHandler): Component { val activity = context.androidContext as CreateNoteActivity return Row.create(context) - .alignItems(YogaAlign.CENTER) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_title_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.HEADING) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_subject_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.TEXT) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_check_box_white_24dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.CHECKLIST_UNCHECKED) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_format_quote_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.QUOTE) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_more_horiz_white_48dp) - .onClick { } - .isClickDisabled(true) - .clickHandler(toggleButtonClick)) - .build() + .alignItems(YogaAlign.CENTER) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_title_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.HEADING) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_subject_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.TEXT) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_check_box_white_24dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.CHECKLIST_UNCHECKED) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_format_quote_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.QUOTE) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_more_horiz_white_48dp) + .onClick { } + .isClickDisabled(true) + .clickHandler(toggleButtonClick)) + .build() } } @LayoutSpec object NoteCreationMarkdownsBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig, - @Prop toggleButtonClick: EventHandler): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig, + @Prop toggleButtonClick: EventHandler): Component { val activity = context.androidContext as CreateNoteActivity return Row.create(context) - .alignItems(YogaAlign.CENTER) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_title_white_48dp) - .onClick { activity.triggerMarkdown(MarkdownType.HEADER) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_markdown_bold) - .onClick { activity.triggerMarkdown(MarkdownType.BOLD) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_markdown_italics) - .onClick { activity.triggerMarkdown(MarkdownType.ITALICS) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_format_list_bulleted_white_48dp) - .onClick { activity.triggerMarkdown(MarkdownType.UNORDERED) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_more_horiz_white_48dp) - .onClick { } - .isClickDisabled(true) - .clickHandler(toggleButtonClick)) - .build() + .alignItems(YogaAlign.CENTER) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_title_white_48dp) + .onClick { activity.triggerMarkdown(MarkdownType.HEADER) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_markdown_bold) + .onClick { activity.triggerMarkdown(MarkdownType.BOLD) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_markdown_italics) + .onClick { activity.triggerMarkdown(MarkdownType.ITALICS) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_format_list_bulleted_white_48dp) + .onClick { activity.triggerMarkdown(MarkdownType.UNORDERED) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_more_horiz_white_48dp) + .onClick { } + .isClickDisabled(true) + .clickHandler(toggleButtonClick)) + .build() } } - @LayoutSpec object NoteCreationAllSegmentsBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig): Component { val activity = context.androidContext as CreateNoteActivity return Row.create(context) - .alignSelf(YogaAlign.CENTER) - .alignItems(YogaAlign.CENTER) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_title_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.HEADING) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_title_white_48dp) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding_subsize) - .onClick { activity.addEmptyItemAtFocused(FormatType.SUB_HEADING) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_subject_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.TEXT) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_check_box_white_24dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.CHECKLIST_UNCHECKED) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_format_quote_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.QUOTE) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_code_white_48dp) - .onClick { activity.addEmptyItemAtFocused(FormatType.CODE) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_image_gallery) - .onClick { activity.addEmptyItemAtFocused(FormatType.IMAGE) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_format_separator) - .onClick { activity.addEmptyItemAtFocused(FormatType.SEPARATOR) }) - .build() + .alignSelf(YogaAlign.CENTER) + .alignItems(YogaAlign.CENTER) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_title_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.HEADING) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_title_white_48dp) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding_subsize) + .onClick { activity.addEmptyItemAtFocused(FormatType.SUB_HEADING) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_subject_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.TEXT) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_check_box_white_24dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.CHECKLIST_UNCHECKED) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_format_quote_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.QUOTE) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_code_white_48dp) + .onClick { activity.addEmptyItemAtFocused(FormatType.CODE) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_image_gallery) + .onClick { activity.addEmptyItemAtFocused(FormatType.IMAGE) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_format_separator) + .onClick { activity.addEmptyItemAtFocused(FormatType.SEPARATOR) }) + .build() } } @LayoutSpec object NoteCreationAllMarkdownsBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig): Component { val activity = context.androidContext as CreateNoteActivity return Row.create(context) - .alignSelf(YogaAlign.CENTER) - .alignItems(YogaAlign.CENTER) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_title_white_48dp) - .onClick { activity.triggerMarkdown(MarkdownType.HEADER) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_title_white_48dp) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding_subsize) - .onClick { activity.triggerMarkdown(MarkdownType.SUB_HEADER) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_markdown_bold) - .onClick { activity.triggerMarkdown(MarkdownType.BOLD) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_markdown_italics) - .onClick { activity.triggerMarkdown(MarkdownType.ITALICS) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_check_box_white_24dp) - .onClick { activity.triggerMarkdown(MarkdownType.CHECKLIST_UNCHECKED) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_format_list_bulleted_white_48dp) - .onClick { activity.triggerMarkdown(MarkdownType.UNORDERED) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_markdown_underline) - .onClick { activity.triggerMarkdown(MarkdownType.UNDERLINE) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_code_white_48dp) - .onClick { activity.triggerMarkdown(MarkdownType.CODE_BLOCK) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.icon_code_block) - .onClick { activity.triggerMarkdown(MarkdownType.CODE) }) - .child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_markdown_strikethrough) - .onClick { activity.triggerMarkdown(MarkdownType.STRIKE_THROUGH) }) - .build() + .alignSelf(YogaAlign.CENTER) + .alignItems(YogaAlign.CENTER) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_title_white_48dp) + .onClick { activity.triggerMarkdown(MarkdownType.HEADER) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_title_white_48dp) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding_subsize) + .onClick { activity.triggerMarkdown(MarkdownType.SUB_HEADER) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_markdown_bold) + .onClick { activity.triggerMarkdown(MarkdownType.BOLD) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_markdown_italics) + .onClick { activity.triggerMarkdown(MarkdownType.ITALICS) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_check_box_white_24dp) + .onClick { activity.triggerMarkdown(MarkdownType.CHECKLIST_UNCHECKED) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_format_list_bulleted_white_48dp) + .onClick { activity.triggerMarkdown(MarkdownType.UNORDERED) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_markdown_underline) + .onClick { activity.triggerMarkdown(MarkdownType.UNDERLINE) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_code_white_48dp) + .onClick { activity.triggerMarkdown(MarkdownType.CODE_BLOCK) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.icon_code_block) + .onClick { activity.triggerMarkdown(MarkdownType.CODE) }) + .child(bottomBarRoundIcon(context, colorConfig) + .iconRes(R.drawable.ic_markdown_strikethrough) + .onClick { activity.triggerMarkdown(MarkdownType.STRIKE_THROUGH) }) + .build() } } @LayoutSpec object NoteViewBottomBarSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop colorConfig: ToolbarColorConfig): Component { + fun onCreate( + context: ComponentContext, + @Prop colorConfig: ToolbarColorConfig): Component { val activity = context.androidContext as ViewAdvancedNoteActivity val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) row.child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.ic_apps_white_48dp) - .onClick { activity.openMoreOptions() }) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.ic_apps_white_48dp) + .onClick { activity.openMoreOptions() }) row.child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.icon_delete) - .onClick { activity.moveItemToTrashOrDelete(activity.note()) }) + .iconRes(R.drawable.icon_delete) + .onClick { activity.moveItemToTrashOrDelete(activity.note()) }) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_content_copy_white_48dp) - .onClick { activity.note().copy(activity) }) + .iconRes(R.drawable.ic_content_copy_white_48dp) + .onClick { activity.note().copy(activity) }) row.child(bottomBarRoundIcon(context, colorConfig) - .iconRes(R.drawable.ic_share_white_48dp) - .onClick { activity.note().share(activity) }) + .iconRes(R.drawable.ic_share_white_48dp) + .onClick { activity.note().share(activity) }) row.child(EmptySpec.create(context).heightDip(1f).flexGrow(1f)) row.child(bottomBarRoundIcon(context, colorConfig) - .bgColor(Color.TRANSPARENT) - .iconRes(R.drawable.ic_edit_white_48dp) - .onClick { activity.openEditor() }) + .bgColor(Color.TRANSPARENT) + .iconRes(R.drawable.ic_edit_white_48dp) + .onClick { activity.openEditor() }) return bottomBarCard(context, row.build(), colorConfig).build() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt index ec37b12e..37de919a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/specs/NoteViewTopBarSpec.kt @@ -13,8 +13,8 @@ object NoteViewTopBarSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) row.child(EmptySpec.create(context).heightDip(10f)) return row.build() } @@ -25,8 +25,8 @@ object NoteCreationTopBarSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) row.child(EmptySpec.create(context).heightDip(10f)) return row.build() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderOptionItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderOptionItem.kt index 7a7c51b8..2844a0f6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderOptionItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderOptionItem.kt @@ -4,12 +4,12 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.database.room.folder.Folder class FolderOptionsItem( - val folder: Folder, - val usages: Int = 0, - val selected: Boolean = false, - val editable: Boolean = false, - val editListener: () -> Unit = {}, - val listener: () -> Unit = {}) { + val folder: Folder, + val usages: Int = 0, + val selected: Boolean = false, + val editable: Boolean = false, + val editListener: () -> Unit = {}, + val listener: () -> Unit = {}) { fun getIcon(): Int = when (selected) { true -> R.drawable.ic_folder diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt index 61434c80..89fcb550 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerItem.kt @@ -8,12 +8,13 @@ import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ColorUtil -class FolderRecyclerItem(context: Context, - val folder: Folder, - val click: () -> Unit = {}, - val longClick: () -> Unit = {}, - val selected: Boolean = false, - contents: Int = -1) : RecyclerItem() { +class FolderRecyclerItem( + context: Context, + val folder: Folder, + val click: () -> Unit = {}, + val longClick: () -> Unit = {}, + val selected: Boolean = false, + contents: Int = -1) : RecyclerItem() { val isLightShaded = ColorUtil.isLightColored(folder.color) val title = folder.title @@ -39,6 +40,5 @@ class FolderRecyclerItem(context: Context, false -> ContextCompat.getColor(context, R.color.light_secondary_text) } - override val type = RecyclerItem.Type.FOLDER } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt index f58e7cb0..d494cb31 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerItem.kt @@ -9,7 +9,6 @@ import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.support.ui.ThemeColorType - class SelectorFolderRecyclerItem(context: Context, val folder: Folder) : RecyclerItem() { val isLightShaded = ColorUtil.isLightColored(folder.color) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt index 117521a2..cde90f99 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/CreateOrEditFolderBottomSheet.kt @@ -3,14 +3,12 @@ package com.maubis.scarlet.base.note.folder.sheet import android.app.Dialog import android.content.Context import android.support.v7.app.AppCompatActivity -import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.widget.EditText import android.widget.TextView import com.google.android.flexbox.FlexboxLayout import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.folder.isUnsaved import com.maubis.scarlet.base.database.room.folder.Folder @@ -23,7 +21,6 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment import com.maubis.scarlet.base.support.utils.getEditorActionListener - class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { var selectedFolder: Folder? = null @@ -73,12 +70,12 @@ class CreateOrEditFolderBottomSheet : ThemedBottomSheetFragment() { } enterFolder.setText(folder.title) enterFolder.setOnEditorActionListener(getEditorActionListener( - runnable = { - val updated = onActionClick(folder, enterFolder.text.toString()) - sheetOnFolderListener(folder, !updated) - dismiss() - return@getEditorActionListener true - })) + runnable = { + val updated = onActionClick(folder, enterFolder.text.toString()) + sheetOnFolderListener(folder, !updated) + dismiss() + return@getEditorActionListener true + })) setColorsList(dialog.context, folder, colorFlexbox) makeBackgroundTransparent(dialog, R.id.root_layout) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt index 68586b87..9eec1fe5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/DeleteFolderBottomSheet.kt @@ -30,48 +30,48 @@ class DeleteFolderBottomSheet : LithoOptionBottomSheet() { val activity = context as AppCompatActivity val options = ArrayList() options.add(LithoOptionsItem( - title = R.string.folder_delete_option_sheet_remove_folder, - subtitle = R.string.folder_delete_option_sheet_remove_folder_details, - icon = R.drawable.icon_delete, - listener = { - folder.delete() - executeForFolderContent(folder) { - it.folder = "" - it.save(activity) - } - - sheetOnFolderListener(folder, true) - dismiss() + title = R.string.folder_delete_option_sheet_remove_folder, + subtitle = R.string.folder_delete_option_sheet_remove_folder_details, + icon = R.drawable.icon_delete, + listener = { + folder.delete() + executeForFolderContent(folder) { + it.folder = "" + it.save(activity) } + + sheetOnFolderListener(folder, true) + dismiss() + } )) options.add(LithoOptionsItem( - title = R.string.folder_delete_option_sheet_remove_folder_content, - subtitle = R.string.folder_delete_option_sheet_remove_folder_content_details, - icon = R.drawable.icon_delete_content, - listener = { - executeForFolderContent(folder) { - it.folder = "" - it.softDelete(activity) - } - - sheetOnFolderListener(folder, false) - dismiss() + title = R.string.folder_delete_option_sheet_remove_folder_content, + subtitle = R.string.folder_delete_option_sheet_remove_folder_content_details, + icon = R.drawable.icon_delete_content, + listener = { + executeForFolderContent(folder) { + it.folder = "" + it.softDelete(activity) } + + sheetOnFolderListener(folder, false) + dismiss() + } )) options.add(LithoOptionsItem( - title = R.string.folder_delete_option_sheet_remove_folder_and_content, - subtitle = R.string.folder_delete_option_sheet_remove_folder_and_content_details, - icon = R.drawable.ic_delete_permanently, - listener = { - folder.delete() - executeForFolderContent(folder) { - it.folder = "" - it.softDelete(activity) - } - - sheetOnFolderListener(folder, true) - dismiss() + title = R.string.folder_delete_option_sheet_remove_folder_and_content, + subtitle = R.string.folder_delete_option_sheet_remove_folder_and_content_details, + icon = R.drawable.ic_delete_permanently, + listener = { + folder.delete() + executeForFolderContent(folder) { + it.folder = "" + it.softDelete(activity) } + + sheetOnFolderListener(folder, true) + dismiss() + } )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt index d7229499..f9a62cfc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt @@ -4,7 +4,11 @@ import android.app.Dialog import android.content.DialogInterface import android.graphics.Typeface import android.support.v7.app.AppCompatActivity -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -13,7 +17,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.FolderBuilder @@ -27,9 +30,9 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity data class FolderOptionsItem( - val folder: Folder, - val isSelected: Boolean = false, - val listener: () -> Unit = {}) + val folder: Folder, + val isSelected: Boolean = false, + val listener: () -> Unit = {}) @LayoutSpec object FolderItemLayoutSpec { @@ -64,28 +67,29 @@ object FolderItemLayoutSpec { } val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 12f) - .child( - RoundIcon.create(context) - .iconRes(icon) - .bgColor(bgColor) - .iconColor(titleColor) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .bgAlpha(bgAlpha) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.END, 16f)) - .child(Text.create(context) - .flexGrow(1f) - .text(option.folder.title) - .textSizeRes(R.dimen.font_size_normal) - .typeface(typeface) - .textStyle(Typeface.BOLD) - .textColor(textColor)) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + RoundIcon.create(context) + .iconRes(icon) + .bgColor(bgColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(bgAlpha) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.END, 16f)) + .child( + Text.create(context) + .flexGrow(1f) + .text(option.folder.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(typeface) + .textStyle(Typeface.BOLD) + .textColor(textColor)) row.clickHandler(OptionItemLayout.onItemClick(context)) return row.build() } @@ -96,7 +100,6 @@ object FolderItemLayoutSpec { } } - abstract class FolderChooserBottomSheetBase : LithoBottomSheet() { var dismissListener: () -> Unit = {} @@ -109,33 +112,34 @@ abstract class FolderChooserBottomSheetBase : LithoBottomSheet() { preComponentRender(componentContext) val activity = context as ThemedActivity val component = Column.create(componentContext) - .widthPercent(100f) + .widthPercent(100f) val foldersComponent = Column.create(componentContext) - .paddingDip(YogaEdge.TOP, 8f) - .paddingDip(YogaEdge.BOTTOM, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.folder_option_change_notebook) - .marginDip(YogaEdge.BOTTOM, 12f)) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.folder_option_change_notebook) + .marginDip(YogaEdge.BOTTOM, 12f)) getFolderOptions().forEach { foldersComponent.child(FolderItemLayout.create(componentContext).option(it)) } val addTag = LithoOptionsItem( - title = R.string.folder_sheet_add_note, - subtitle = 0, - icon = R.drawable.icon_add_notebook, - listener = { - CreateOrEditFolderBottomSheet.openSheet(activity, FolderBuilder().emptyFolder()) { folder, _ -> - onFolderSelected(folder) - reset(activity, dialog) - } - }) + title = R.string.folder_sheet_add_note, + subtitle = 0, + icon = R.drawable.icon_add_notebook, + listener = { + CreateOrEditFolderBottomSheet.openSheet(activity, FolderBuilder().emptyFolder()) { folder, _ -> + onFolderSelected(folder) + reset(activity, dialog) + } + }) foldersComponent.child(OptionItemLayout.create(componentContext) - .option(addTag) - .backgroundRes(R.drawable.accent_rounded_bg) - .marginDip(YogaEdge.TOP, 16f) - .onClick { addTag.listener() }) + .option(addTag) + .backgroundRes(R.drawable.accent_rounded_bg) + .marginDip(YogaEdge.TOP, 16f) + .onClick { addTag.listener() }) component.child(foldersComponent) return component.build() @@ -145,14 +149,15 @@ abstract class FolderChooserBottomSheetBase : LithoBottomSheet() { val activity = context as AppCompatActivity val options = ArrayList() for (folder in CoreConfig.foldersDb.getAll()) { - options.add(FolderOptionsItem( + options.add( + FolderOptionsItem( folder = folder, listener = { onFolderSelected(folder) reset(activity, dialog) }, isSelected = isFolderSelected(folder) - )) + )) } options.sortByDescending { if (it.isSelected) 1 else 0 } return options diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt index 14e56ee8..2133998f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/FormatControllerList.kt @@ -4,100 +4,106 @@ import com.github.bijoysingh.starter.recyclerview.MultiRecyclerViewControllerIte import com.maubis.scarlet.base.R import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType -import com.maubis.scarlet.base.note.formats.recycler.* +import com.maubis.scarlet.base.note.formats.recycler.FormatBulletViewHolder +import com.maubis.scarlet.base.note.formats.recycler.FormatImageViewHolder +import com.maubis.scarlet.base.note.formats.recycler.FormatListViewHolder +import com.maubis.scarlet.base.note.formats.recycler.FormatQuoteViewHolder +import com.maubis.scarlet.base.note.formats.recycler.FormatSeparatorViewHolder +import com.maubis.scarlet.base.note.formats.recycler.FormatTextViewHolder +import com.maubis.scarlet.base.note.formats.recycler.NullFormatHolder import java.util.* fun getFormatControllerItems(): List> { val list = ArrayList>() list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.TAG.ordinal) - .layoutFile(R.layout.item_format_tag) - .holderClass(FormatTextViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.TAG.ordinal) + .layoutFile(R.layout.item_format_tag) + .holderClass(FormatTextViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.TEXT.ordinal) - .layoutFile(R.layout.item_format_text) - .holderClass(FormatTextViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.TEXT.ordinal) + .layoutFile(R.layout.item_format_text) + .holderClass(FormatTextViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.HEADING.ordinal) - .layoutFile(R.layout.item_format_heading) - .holderClass(FormatTextViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.HEADING.ordinal) + .layoutFile(R.layout.item_format_heading) + .holderClass(FormatTextViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.SUB_HEADING.ordinal) - .layoutFile(R.layout.item_format_heading) - .holderClass(FormatTextViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.SUB_HEADING.ordinal) + .layoutFile(R.layout.item_format_heading) + .holderClass(FormatTextViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.HEADING_3.ordinal) - .layoutFile(R.layout.item_format_heading) - .holderClass(FormatTextViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.HEADING_3.ordinal) + .layoutFile(R.layout.item_format_heading) + .holderClass(FormatTextViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.QUOTE.ordinal) - .layoutFile(R.layout.item_format_quote) - .holderClass(FormatQuoteViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.QUOTE.ordinal) + .layoutFile(R.layout.item_format_quote) + .holderClass(FormatQuoteViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.CODE.ordinal) - .layoutFile(R.layout.item_format_code) - .holderClass(FormatTextViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.CODE.ordinal) + .layoutFile(R.layout.item_format_code) + .holderClass(FormatTextViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.BULLET_1.ordinal) - .layoutFile(R.layout.item_format_bullet) - .holderClass(FormatBulletViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.BULLET_1.ordinal) + .layoutFile(R.layout.item_format_bullet) + .holderClass(FormatBulletViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.BULLET_2.ordinal) - .layoutFile(R.layout.item_format_bullet) - .holderClass(FormatBulletViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.BULLET_2.ordinal) + .layoutFile(R.layout.item_format_bullet) + .holderClass(FormatBulletViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.BULLET_3.ordinal) - .layoutFile(R.layout.item_format_bullet) - .holderClass(FormatBulletViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.BULLET_3.ordinal) + .layoutFile(R.layout.item_format_bullet) + .holderClass(FormatBulletViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.CHECKLIST_CHECKED.ordinal) - .layoutFile(R.layout.item_format_list) - .holderClass(FormatListViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.CHECKLIST_CHECKED.ordinal) + .layoutFile(R.layout.item_format_list) + .holderClass(FormatListViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.CHECKLIST_UNCHECKED.ordinal) - .layoutFile(R.layout.item_format_list) - .holderClass(FormatListViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.CHECKLIST_UNCHECKED.ordinal) + .layoutFile(R.layout.item_format_list) + .holderClass(FormatListViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.IMAGE.ordinal) - .layoutFile(R.layout.item_format_image) - .holderClass(FormatImageViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.IMAGE.ordinal) + .layoutFile(R.layout.item_format_image) + .holderClass(FormatImageViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.SEPARATOR.ordinal) - .layoutFile(R.layout.item_format_separator) - .holderClass(FormatSeparatorViewHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.SEPARATOR.ordinal) + .layoutFile(R.layout.item_format_separator) + .holderClass(FormatSeparatorViewHolder::class.java) + .build()) list.add( - MultiRecyclerViewControllerItem.Builder() - .viewType(FormatType.EMPTY.ordinal) - .layoutFile(R.layout.item_format_fab_space) - .holderClass(NullFormatHolder::class.java) - .build()) + MultiRecyclerViewControllerItem.Builder() + .viewType(FormatType.EMPTY.ordinal) + .layoutFile(R.layout.item_format_fab_space) + .holderClass(NullFormatHolder::class.java) + .build()) return list } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt index 9f97695f..14c6bcac 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatListViewHolder.kt @@ -19,11 +19,11 @@ class FormatListViewHolder(context: Context, view: View) : FormatTextViewHolder( init { edit.setOnEditorActionListener(getEditorActionListener( - runnable = { - activity.createOrChangeToNextFormat(format) - true - }, - preConditions = {!edit.isFocused } + runnable = { + activity.createOrChangeToNextFormat(format) + true + }, + preConditions = { !edit.isFocused } )) edit.imeOptions = EditorInfo.IME_ACTION_DONE edit.setRawInputType(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt index 57d16ae3..198b101d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatTextViewHolder.kt @@ -36,10 +36,10 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder edit.addTextChangedListener(this) edit.onFocusChangeListener = View.OnFocusChangeListener { _, _ -> activity.focusedFormat = format } edit.setRawInputType( - InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - or InputType.TYPE_TEXT_FLAG_MULTI_LINE - or InputType.TYPE_CLASS_TEXT - or InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + or InputType.TYPE_TEXT_FLAG_MULTI_LINE + or InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE ) } @@ -154,11 +154,11 @@ open class FormatTextViewHolder(context: Context, view: View) : FormatViewHolder FormatType.HEADING, FormatType.SUB_HEADING, FormatType.HEADING_3 - -> context.getString(R.string.format_hint_heading) + -> context.getString(R.string.format_hint_heading) FormatType.NUMBERED_LIST, FormatType.CHECKLIST_UNCHECKED, FormatType.CHECKLIST_CHECKED - -> context.getString(R.string.format_hint_list) + -> context.getString(R.string.format_hint_list) FormatType.CODE -> context.getString(R.string.format_hint_code) FormatType.QUOTE -> context.getString(R.string.format_hint_quote) else -> "" diff --git a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt index 518ef901..ca5ed93e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/formats/recycler/FormatViewHolderBase.kt @@ -6,7 +6,6 @@ import android.support.v4.content.ContextCompat import android.view.View import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.format.Format import com.maubis.scarlet.base.core.format.FormatType @@ -25,17 +24,16 @@ const val KEY_EDITABLE = "KEY_EDITABLE" const val KEY_NOTE_COLOR = "KEY_NOTE_COLOR" data class FormatViewHolderConfig( - val editable: Boolean, - val isMarkdownEnabled: Boolean, - val fontSize: Float, - val backgroundColor: Int, - val secondaryTextColor: Int, - val tertiaryTextColor: Int, - val iconColor: Int, - val hintTextColor: Int, - val accentColor: Int, - val noteUUID: String) - + val editable: Boolean, + val isMarkdownEnabled: Boolean, + val fontSize: Float, + val backgroundColor: Int, + val secondaryTextColor: Int, + val tertiaryTextColor: Int, + val iconColor: Int, + val hintTextColor: Int, + val accentColor: Int, + val noteUUID: String) abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerViewHolder(context, view) { @@ -73,32 +71,32 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView } } val - config = FormatViewHolderConfig( - editable = !(extra != null - && extra.containsKey(KEY_EDITABLE) - && !extra.getBoolean(KEY_EDITABLE)), - isMarkdownEnabled = (extra == null - || extra.getBoolean(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) - || data.forcedMarkdown) && (data.formatType != FormatType.CODE), - fontSize = { - val fontSize = extra?.getInt(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) - ?: TEXT_SIZE_DEFAULT - when (data.formatType) { - FormatType.HEADING -> fontSize.toFloat() + 4 - FormatType.SUB_HEADING -> fontSize.toFloat() + 2 - else -> fontSize.toFloat() - } - }(), - backgroundColor = when (data.formatType) { - FormatType.CODE, FormatType.IMAGE -> sAppTheme.get(context, R.color.code_light, R.color.code_dark) - else -> ContextCompat.getColor(context, R.color.transparent) - }, - secondaryTextColor = secondaryTextColor, - tertiaryTextColor = tertiaryTextColor, - iconColor = iconColor, - hintTextColor = hintTextColor, - accentColor = linkColor, - noteUUID = extra?.getString(INTENT_KEY_NOTE_ID) ?: "default") + config = FormatViewHolderConfig( + editable = !(extra != null + && extra.containsKey(KEY_EDITABLE) + && !extra.getBoolean(KEY_EDITABLE)), + isMarkdownEnabled = (extra == null + || extra.getBoolean(SettingsOptionsBottomSheet.KEY_MARKDOWN_ENABLED, true) + || data.forcedMarkdown) && (data.formatType != FormatType.CODE), + fontSize = { + val fontSize = extra?.getInt(STORE_KEY_TEXT_SIZE, TEXT_SIZE_DEFAULT) + ?: TEXT_SIZE_DEFAULT + when (data.formatType) { + FormatType.HEADING -> fontSize.toFloat() + 4 + FormatType.SUB_HEADING -> fontSize.toFloat() + 2 + else -> fontSize.toFloat() + } + }(), + backgroundColor = when (data.formatType) { + FormatType.CODE, FormatType.IMAGE -> sAppTheme.get(context, R.color.code_light, R.color.code_dark) + else -> ContextCompat.getColor(context, R.color.transparent) + }, + secondaryTextColor = secondaryTextColor, + tertiaryTextColor = tertiaryTextColor, + iconColor = iconColor, + hintTextColor = hintTextColor, + accentColor = linkColor, + noteUUID = extra?.getString(INTENT_KEY_NOTE_ID) ?: "default") populate(data, config) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt index bcc9116c..03e4772f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteAppAdapter.kt @@ -16,7 +16,10 @@ import java.util.* class NoteAppAdapter : MultiRecyclerViewAdapter { - @JvmOverloads constructor(context: Context, staggered: Boolean = false, isTablet: Boolean = false) : super(context, getRecyclerItemControllerList(staggered, isTablet)) {} + @JvmOverloads + constructor(context: Context, staggered: Boolean = false, isTablet: Boolean = false) : super( + context, getRecyclerItemControllerList(staggered, isTablet)) { + } constructor(context: Context, list: List>) : super(context, list) {} @@ -26,35 +29,41 @@ class NoteAppAdapter : MultiRecyclerViewAdapter { } fun getRecyclerItemControllerList( - staggered: Boolean, - isTablet: Boolean): List> { + staggered: Boolean, + isTablet: Boolean): List> { val list = ArrayList>() - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.NOTE.ordinal) .layoutFile(if (staggered && !isTablet) R.layout.item_note_staggered else R.layout.item_note) .holderClass(NoteRecyclerHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) .holderClass(EmptyRecyclerHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.INFORMATION.ordinal) .layoutFile(R.layout.item_information) .holderClass(InformationRecyclerHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.FILE.ordinal) .layoutFile(R.layout.item_import_file) .holderClass(FileImportViewHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.FOLDER.ordinal) .layoutFile(R.layout.item_folder) .holderClass(FolderRecyclerHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.TOOLBAR.ordinal) .layoutFile(R.layout.toolbar_main) .holderClass(ToolbarMainRecyclerHolder::class.java) @@ -63,20 +72,23 @@ fun getRecyclerItemControllerList( } fun getSelectableRecyclerItemControllerList( - staggered: Boolean, - isTablet: Boolean): List> { + staggered: Boolean, + isTablet: Boolean): List> { val list = ArrayList>() - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.NOTE.ordinal) .layoutFile(if (staggered && !isTablet) R.layout.item_note_staggered else R.layout.item_note) .holderClass(SelectableNoteRecyclerViewHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.FOLDER.ordinal) .layoutFile(R.layout.item_selector_folder) .holderClass(SelectorFolderRecyclerHolder::class.java) .build()) - list.add(MultiRecyclerViewControllerItem.Builder() + list.add( + MultiRecyclerViewControllerItem.Builder() .viewType(RecyclerItem.Type.EMPTY.ordinal) .layoutFile(R.layout.item_no_notes) .holderClass(EmptyRecyclerHolder::class.java) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt index f3d59282..61ee3511 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerHolder.kt @@ -48,9 +48,9 @@ class NoteRecyclerHolder(context: Context, view: View) : NoteRecyclerViewHolderB private fun actionOrUnlockNote(data: Note, runnable: Runnable) { if (context is ThemedActivity && data.locked) { openUnlockSheet( - activity = context as ThemedActivity, - onUnlockSuccess = { runnable.run() }, - onUnlockFailure = { actionOrUnlockNote(data, runnable) }) + activity = context as ThemedActivity, + onUnlockSuccess = { runnable.run() }, + onUnlockFailure = { actionOrUnlockNote(data, runnable) }) return } else if (data.locked) { return diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt index 29675dee..24199ff5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/ReminderJob.kt @@ -17,7 +17,6 @@ import com.maubis.scarlet.base.support.utils.maybeThrow import java.util.* import java.util.concurrent.TimeUnit - class ReminderJob : Job() { override fun onRunJob(params: Params): Job.Result { @@ -34,9 +33,9 @@ class ReminderJob : Job() { val reminder = note.getReminderV2() if (reminder?.interval == ReminderInterval.DAILY) { val reminderV2 = Reminder( - 0, - nextJobTimestamp(reminder.timestamp, System.currentTimeMillis()), - ReminderInterval.DAILY) + 0, + nextJobTimestamp(reminder.timestamp, System.currentTimeMillis()), + ReminderInterval.DAILY) reminderV2.uid = scheduleJob(note.uuid, reminderV2) note.setReminderV2(reminderV2) note.saveWithoutSync(context) @@ -65,10 +64,10 @@ class ReminderJob : Job() { } return JobRequest.Builder(ReminderJob.TAG) - .setExact(deltaTime) - .setExtras(extras) - .build() - .schedule() + .setExact(deltaTime) + .setExtras(extras) + .build() + .schedule() } fun nextJobTimestamp(timestamp: Long, currentTimestamp: Long): Long { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt index 5b95a83f..f307ce2d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/reminders/sheet/ReminderBottomSheet.kt @@ -8,7 +8,6 @@ import android.widget.TextView import com.github.bijoysingh.starter.util.DateFormatter import com.github.bijoysingh.uibasics.views.UIActionView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.note.Reminder import com.maubis.scarlet.base.core.note.ReminderInterval @@ -25,14 +24,13 @@ import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment import com.maubis.scarlet.base.support.utils.sDateFormat import java.util.* - class ReminderBottomSheet : ThemedBottomSheetFragment() { var selectedNote: Note? = null var reminder: Reminder = Reminder( - 0, - Calendar.getInstance().timeInMillis, - ReminderInterval.ONCE) + 0, + Calendar.getInstance().timeInMillis, + ReminderInterval.ONCE) override fun getBackgroundView(): Int { return R.id.container_layout @@ -51,9 +49,9 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { val calendar = Calendar.getInstance() reminder = note.getReminderV2() ?: Reminder( - 0, - calendar.timeInMillis, - ReminderInterval.ONCE) + 0, + calendar.timeInMillis, + ReminderInterval.ONCE) val isNewReminder = reminder.uid == 0 if (isNewReminder) { @@ -132,64 +130,64 @@ class ReminderBottomSheet : ThemedBottomSheetFragment() { fun openFrequencyDialog() { val isSelected = fun(interval: ReminderInterval): Boolean = interval == reminder.interval com.maubis.scarlet.base.support.sheets.openSheet( - themedActivity() as ThemedActivity, - GenericOptionsBottomSheet().apply { - title = R.string.reminder_sheet_repeat - options = arrayListOf( - LithoChooseOptionsItem( - title = getReminderIntervalLabel(ReminderInterval.ONCE), - listener = { - reminder.interval = ReminderInterval.ONCE - setContent(reminder) - }, - selected = isSelected(ReminderInterval.ONCE) - ), - LithoChooseOptionsItem( - title = getReminderIntervalLabel(ReminderInterval.DAILY), - listener = { - reminder.interval = ReminderInterval.DAILY - setContent(reminder) - }, - selected = isSelected(ReminderInterval.DAILY) - ) + themedActivity() as ThemedActivity, + GenericOptionsBottomSheet().apply { + title = R.string.reminder_sheet_repeat + options = arrayListOf( + LithoChooseOptionsItem( + title = getReminderIntervalLabel(ReminderInterval.ONCE), + listener = { + reminder.interval = ReminderInterval.ONCE + setContent(reminder) + }, + selected = isSelected(ReminderInterval.ONCE) + ), + LithoChooseOptionsItem( + title = getReminderIntervalLabel(ReminderInterval.DAILY), + listener = { + reminder.interval = ReminderInterval.DAILY + setContent(reminder) + }, + selected = isSelected(ReminderInterval.DAILY) ) - } + ) + } ) } fun openDatePickerDialog() { val calendar = reminder.toCalendar() val dialog = DatePickerDialog( - themedContext(), - R.style.DialogTheme, - DatePickerDialog.OnDateSetListener { _, year, month, day -> - calendar.set(Calendar.YEAR, year) - calendar.set(Calendar.MONTH, month) - calendar.set(Calendar.DAY_OF_MONTH, day) - reminder.timestamp = calendar.timeInMillis - setContent(reminder) - }, - calendar.get(Calendar.YEAR), - calendar.get(Calendar.MONTH), - calendar.get(Calendar.DAY_OF_MONTH)) + themedContext(), + R.style.DialogTheme, + DatePickerDialog.OnDateSetListener { _, year, month, day -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, day) + reminder.timestamp = calendar.timeInMillis + setContent(reminder) + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)) dialog.show() } fun openTimePickerDialog() { val calendar = reminder.toCalendar() val dialog = TimePickerDialog( - themedContext(), - R.style.DialogTheme, - TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute -> - calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) - calendar.set(Calendar.MINUTE, minute) - calendar.set(Calendar.SECOND, 0) - reminder.timestamp = calendar.timeInMillis - setContent(reminder) - }, - calendar.get(Calendar.HOUR_OF_DAY), - calendar.get(Calendar.MINUTE), - false) + themedContext(), + R.style.DialogTheme, + TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) + calendar.set(Calendar.SECOND, 0) + reminder.timestamp = calendar.timeInMillis + setContent(reminder) + }, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + false) dialog.show() } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt index 20228f5d..e5bdaeaa 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectNotesActivity.kt @@ -49,9 +49,9 @@ class SelectNotesActivity : SelectableNotesActivityBase() { primaryFab.setOnClickListener { runTextFunction { text -> IntentUtils.ShareBuilder(this) - .setChooserText(getString(R.string.share_using)) - .setText(text) - .share() + .setChooserText(getString(R.string.share_using)) + .setText(text) + .share() } } secondaryFab.setOnClickListener { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt index b6cab84e..30066ca0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/activity/SelectableNotesActivityBase.kt @@ -11,7 +11,6 @@ import android.widget.TextView import com.github.bijoysingh.starter.async.MultiAsyncTask import com.github.bijoysingh.starter.recyclerview.RecyclerViewBuilder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme @@ -22,7 +21,11 @@ import com.maubis.scarlet.base.note.folder.SelectorFolderRecyclerItem import com.maubis.scarlet.base.note.recycler.NoteAppAdapter import com.maubis.scarlet.base.note.recycler.NoteRecyclerItem import com.maubis.scarlet.base.note.recycler.getSelectableRecyclerItemControllerList -import com.maubis.scarlet.base.settings.sheet.* +import com.maubis.scarlet.base.settings.sheet.STORE_KEY_LINE_COUNT +import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet +import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet +import com.maubis.scarlet.base.settings.sheet.UISettingsOptionsBottomSheet +import com.maubis.scarlet.base.settings.sheet.sNoteItemLineCount import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.SecuredActivity import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -45,8 +48,8 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct override fun run(): List { val sorting = SortingOptionsBottomSheet.getSortingState() val notes = sort(getNotes(), sorting) - .sortedBy { it.folder } - .map { NoteRecyclerItem(this@SelectableNotesActivityBase, it) } + .sortedBy { it.folder } + .map { NoteRecyclerItem(this@SelectableNotesActivityBase, it) } if (notes.isEmpty()) { return notes @@ -103,10 +106,10 @@ abstract class SelectableNotesActivityBase : SecuredActivity(), INoteSelectorAct adapter = NoteAppAdapter(this, getSelectableRecyclerItemControllerList(staggeredView, isTablet)) adapter.setExtra(adapterExtra) recyclerView = RecyclerViewBuilder(this) - .setView(this, R.id.recycler_view) - .setAdapter(adapter) - .setLayoutManager(getLayoutManager(staggeredView, isTablet)) - .build() + .setView(this, R.id.recycler_view) + .setAdapter(adapter) + .setLayoutManager(getLayoutManager(staggeredView, isTablet)) + .build() } override fun notifyThemeChange() { diff --git a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt index 00d6d46f..9f74e8f2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/selection/sheet/SelectedNotesOptionsBottomSheet.kt @@ -12,8 +12,12 @@ import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getFormats import com.maubis.scarlet.base.main.sheets.AlertBottomSheet import com.maubis.scarlet.base.main.sheets.AlertSheetConfig -import com.maubis.scarlet.base.note.* +import com.maubis.scarlet.base.note.addTag +import com.maubis.scarlet.base.note.delete import com.maubis.scarlet.base.note.folder.sheet.SelectedFolderChooseOptionsBottomSheet +import com.maubis.scarlet.base.note.mark +import com.maubis.scarlet.base.note.removeTag +import com.maubis.scarlet.base.note.save import com.maubis.scarlet.base.note.selection.activity.SelectNotesActivity import com.maubis.scarlet.base.note.tag.sheet.SelectedTagChooserBottomSheet import com.maubis.scarlet.base.security.sheets.openUnlockSheet @@ -38,7 +42,8 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { val options = ArrayList() val allItemsInTrash = !activity.getAllSelectedNotes().any { it.state !== NoteState.TRASH.name } - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.restore_note, icon = R.drawable.ic_restore, listener = lockAwareFunctionRunner(activity) { @@ -48,10 +53,11 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = allItemsInTrash - )) + )) val allItemsInFavourite = !activity.getAllSelectedNotes().any { it.state !== NoteState.FAVOURITE.name } - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.not_favourite_note, icon = R.drawable.ic_favorite_white_48dp, listener = lockAwareFunctionRunner(activity) { @@ -61,8 +67,9 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = allItemsInFavourite - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.favourite_note, icon = R.drawable.ic_favorite_border_white_48dp, listener = lockAwareFunctionRunner(activity) { @@ -72,10 +79,11 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = !allItemsInFavourite - )) + )) val allItemsInArchived = !activity.getAllSelectedNotes().any { it.state !== NoteState.ARCHIVED.name } - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.unarchive_note, icon = R.drawable.ic_archive_white_48dp, listener = lockAwareFunctionRunner(activity) { @@ -85,8 +93,9 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = allItemsInArchived - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.archive_note, icon = R.drawable.ic_archive_white_48dp, listener = lockAwareFunctionRunner(activity) { @@ -96,30 +105,31 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = !allItemsInArchived - )) + )) options.add(GridSectionOptionItem( - label = R.string.send_note, - icon = R.drawable.ic_share_white_48dp, - listener = lockAwareFunctionRunner(activity) { - activity.runTextFunction { - IntentUtils.ShareBuilder(activity) - .setChooserText(getString(R.string.share_using)) - .setText(it) - .share() - } + label = R.string.send_note, + icon = R.drawable.ic_share_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runTextFunction { + IntentUtils.ShareBuilder(activity) + .setChooserText(getString(R.string.share_using)) + .setText(it) + .share() } + } )) options.add(GridSectionOptionItem( - label = R.string.copy_note, - icon = R.drawable.ic_content_copy_white_48dp, - listener = lockAwareFunctionRunner(activity) { - activity.runTextFunction { - TextUtils.copyToClipboard(activity, it) - } - activity.finish() + label = R.string.copy_note, + icon = R.drawable.ic_content_copy_white_48dp, + listener = lockAwareFunctionRunner(activity) { + activity.runTextFunction { + TextUtils.copyToClipboard(activity, it) } + activity.finish() + } )) - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.trash_note, icon = R.drawable.ic_delete_white_48dp, listener = lockAwareFunctionRunner(activity) { @@ -129,11 +139,11 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = !allItemsInTrash - )) + )) return GridSectionItem( - options = options, - sectionColor = ContextCompat.getColor(activity, R.color.material_blue_800)) + options = options, + sectionColor = ContextCompat.getColor(activity, R.color.material_blue_800)) } private fun getSecondaryActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { @@ -141,48 +151,48 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { val options = ArrayList() options.add(GridSectionOptionItem( - label = R.string.change_tags, - icon = R.drawable.ic_action_tags, - listener = lockAwareFunctionRunner(activity) { - openSheet(activity, SelectedTagChooserBottomSheet().apply { - onActionListener = { tag, selectTag -> - activity.runNoteFunction { - when (selectTag) { - true -> it.addTag(tag) - false -> it.removeTag(tag) - } - it.save(activity) + label = R.string.change_tags, + icon = R.drawable.ic_action_tags, + listener = lockAwareFunctionRunner(activity) { + openSheet(activity, SelectedTagChooserBottomSheet().apply { + onActionListener = { tag, selectTag -> + activity.runNoteFunction { + when (selectTag) { + true -> it.addTag(tag) + false -> it.removeTag(tag) } - activity.finish() + it.save(activity) } - }) - } + activity.finish() + } + }) + } )) options.add(GridSectionOptionItem( - label = R.string.folder_option_change_notebook, - icon = R.drawable.ic_folder, - listener = lockAwareFunctionRunner(activity) { - openSheet(activity, SelectedFolderChooseOptionsBottomSheet().apply { - this.dismissListener = {} - this.onActionListener = { folder, selectFolder -> - activity.runNoteFunction { - when (selectFolder) { - true -> it.folder = folder.uuid - false -> it.folder = "" - } - it.save(activity) + label = R.string.folder_option_change_notebook, + icon = R.drawable.ic_folder, + listener = lockAwareFunctionRunner(activity) { + openSheet(activity, SelectedFolderChooseOptionsBottomSheet().apply { + this.dismissListener = {} + this.onActionListener = { folder, selectFolder -> + activity.runNoteFunction { + when (selectFolder) { + true -> it.folder = folder.uuid + false -> it.folder = "" } - activity.finish() + it.save(activity) } - }) - } + activity.finish() + } + }) + } )) - val allLocked = !activity.getAllSelectedNotes().any { !it.locked } - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.lock_note, icon = R.drawable.ic_action_lock, listener = lockAwareFunctionRunner(activity) { @@ -193,8 +203,9 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = !allLocked - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.unlock_note, icon = R.drawable.ic_action_unlock, listener = lockAwareFunctionRunner(activity) { @@ -205,11 +216,11 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = allLocked - )) + )) return GridSectionItem( - options = options, - sectionColor = ContextCompat.getColor(activity, R.color.material_red_800)) + options = options, + sectionColor = ContextCompat.getColor(activity, R.color.material_red_800)) } private fun getTertiaryActions(componentContext: ComponentContext, dialog: Dialog): GridSectionItem { @@ -217,7 +228,8 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { val options = ArrayList() val allItemsPinned = !activity.getAllSelectedNotes().any { !it.pinned } - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.pin_note, icon = R.drawable.ic_pin, listener = lockAwareFunctionRunner(activity) { @@ -228,8 +240,9 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = !allItemsPinned - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.unpin_note, icon = R.drawable.ic_pin, listener = lockAwareFunctionRunner(activity) { @@ -240,54 +253,55 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = allItemsPinned - )) + )) options.add(GridSectionOptionItem( - label = R.string.merge_notes, - icon = R.drawable.ic_merge_note, - listener = lockAwareFunctionRunner(activity) { - val selectedNotes = activity.getOrderedSelectedNotes().toMutableList() - if (selectedNotes.isEmpty()) { - return@lockAwareFunctionRunner - } + label = R.string.merge_notes, + icon = R.drawable.ic_merge_note, + listener = lockAwareFunctionRunner(activity) { + val selectedNotes = activity.getOrderedSelectedNotes().toMutableList() + if (selectedNotes.isEmpty()) { + return@lockAwareFunctionRunner + } - val note = selectedNotes.firstOrNull() - if (note === null) { - return@lockAwareFunctionRunner - } + val note = selectedNotes.firstOrNull() + if (note === null) { + return@lockAwareFunctionRunner + } - val formats = note.getFormats().toMutableList() - selectedNotes.removeAt(0) - for (noteToAdd in selectedNotes) { - formats.addAll(noteToAdd.getFormats()) - noteToAdd.delete(activity) - } - note.description = FormatBuilder().getDescription(sectionPreservingSort(formats)) - note.save(activity) - activity.finish() + val formats = note.getFormats().toMutableList() + selectedNotes.removeAt(0) + for (noteToAdd in selectedNotes) { + formats.addAll(noteToAdd.getFormats()) + noteToAdd.delete(activity) } + note.description = FormatBuilder().getDescription(sectionPreservingSort(formats)) + note.save(activity) + activity.finish() + } )) options.add(GridSectionOptionItem( - label = R.string.delete_note_permanently, - icon = R.drawable.ic_delete_permanently, - listener = lockAwareFunctionRunner(activity) { - openSheet(activity, AlertBottomSheet().apply { - config = AlertSheetConfig( - description = R.string.delete_sheet_delete_selected_notes_permanently, - onPositiveClick = { - activity.runNoteFunction { - it.delete(activity) - } - activity.finish() - } - ) - }) - } + label = R.string.delete_note_permanently, + icon = R.drawable.ic_delete_permanently, + listener = lockAwareFunctionRunner(activity) { + openSheet(activity, AlertBottomSheet().apply { + config = AlertSheetConfig( + description = R.string.delete_sheet_delete_selected_notes_permanently, + onPositiveClick = { + activity.runNoteFunction { + it.delete(activity) + } + activity.finish() + } + ) + }) + } )) val allBackupDisabled = !activity.getAllSelectedNotes().any { !it.disableBackup } - options.add(GridSectionOptionItem( + options.add( + GridSectionOptionItem( label = R.string.backup_note_enable, icon = R.drawable.ic_action_backup, listener = lockAwareFunctionRunner(activity) { @@ -298,8 +312,9 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = allBackupDisabled - )) - options.add(GridSectionOptionItem( + )) + options.add( + GridSectionOptionItem( label = R.string.backup_note_disable, icon = R.drawable.ic_action_backup_no, listener = lockAwareFunctionRunner(activity) { @@ -310,24 +325,24 @@ class SelectedNotesOptionsBottomSheet : GridOptionBottomSheet() { activity.finish() }, visible = !allBackupDisabled - )) + )) return GridSectionItem( - options = options, - sectionColor = ContextCompat.getColor(activity, R.color.material_teal_800)) + options = options, + sectionColor = ContextCompat.getColor(activity, R.color.material_teal_800)) } private fun lockAwareFunctionRunner( - activity: SelectNotesActivity, - listener: () -> Unit): () -> Unit = { + activity: SelectNotesActivity, + listener: () -> Unit): () -> Unit = { val hasLockedNote = activity.getAllSelectedNotes().any { it.locked } when { hasLockedNote -> openUnlockSheet( - activity = activity, - onUnlockSuccess = { - listener() - dismiss() - }, - onUnlockFailure = {}) + activity = activity, + onUnlockSuccess = { + listener() + dismiss() + }, + onUnlockFailure = {}) else -> { listener() dismiss() diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/TagOptionsItem.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/TagOptionsItem.kt index efcf70f0..6df3bd20 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/TagOptionsItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/TagOptionsItem.kt @@ -6,12 +6,12 @@ import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.support.option.TagOptionsItemBase class TagOptionsItem( - tag: Tag, - usages: Int = 0, - selected: Boolean = false, - editable: Boolean = false, - editListener: View.OnClickListener? = null, - listener: View.OnClickListener) : TagOptionsItemBase(tag, usages, selected, editable, editListener, listener) { + tag: Tag, + usages: Int = 0, + selected: Boolean = false, + editable: Boolean = false, + editListener: View.OnClickListener? = null, + listener: View.OnClickListener) : TagOptionsItemBase(tag, usages, selected, editable, editListener, listener) { override fun getIcon(): Int = when (selected) { true -> R.drawable.ic_action_label diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt index 06b1dcff..86ec1ace 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/CreateOrEditTagBottomSheet.kt @@ -6,7 +6,6 @@ import android.view.View.VISIBLE import android.widget.EditText import android.widget.TextView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.core.tag.isUnsaved import com.maubis.scarlet.base.database.room.tag.Tag @@ -17,7 +16,6 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment import com.maubis.scarlet.base.support.utils.getEditorActionListener - class CreateOrEditTagBottomSheet : ThemedBottomSheetFragment() { var selectedTag: Tag? = null @@ -62,12 +60,12 @@ class CreateOrEditTagBottomSheet : ThemedBottomSheetFragment() { } enterTag.setText(tag.title) enterTag.setOnEditorActionListener(getEditorActionListener( - runnable = { - val updated = onActionClick(tag, enterTag.text.toString()) - sheetOnTagListener(tag, !updated) - dismiss() - return@getEditorActionListener true - })) + runnable = { + val updated = onActionClick(tag, enterTag.text.toString()) + sheetOnTagListener(tag, !updated) + dismiss() + return@getEditorActionListener true + })) makeBackgroundTransparent(dialog, R.id.root_layout) } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt index 91cac4bf..68ece29d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/SelectedTagChooserBottomSheet.kt @@ -25,33 +25,34 @@ class SelectedTagChooserBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val activity = context as SelectNotesActivity val component = Column.create(componentContext) - .widthPercent(100f) + .widthPercent(100f) val tagsComponent = Column.create(componentContext) - .paddingDip(YogaEdge.TOP, 8f) - .paddingDip(YogaEdge.BOTTOM, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.tag_sheet_choose_tag) - .marginDip(YogaEdge.BOTTOM, 12f)) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.tag_sheet_choose_tag) + .marginDip(YogaEdge.BOTTOM, 12f)) getTagOptions().forEach { tagsComponent.child(TagItemLayout.create(componentContext).option(it)) } val addTag = LithoOptionsItem( - title = R.string.tag_sheet_new_tag_button, - subtitle = 0, - icon = R.drawable.icon_add_note, - listener = { - CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { tag, _ -> - onActionListener(tag, true) - reset(activity, dialog) - } - }) + title = R.string.tag_sheet_new_tag_button, + subtitle = 0, + icon = R.drawable.icon_add_note, + listener = { + CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { tag, _ -> + onActionListener(tag, true) + reset(activity, dialog) + } + }) tagsComponent.child(OptionItemLayout.create(componentContext) - .option(addTag) - .backgroundRes(R.drawable.accent_rounded_bg) - .marginDip(YogaEdge.TOP, 16f) - .onClick { addTag.listener() }) + .option(addTag) + .backgroundRes(R.drawable.accent_rounded_bg) + .marginDip(YogaEdge.TOP, 16f) + .onClick { addTag.listener() }) component.child(tagsComponent) return component.build() @@ -75,7 +76,8 @@ class SelectedTagChooserBottomSheet : LithoBottomSheet() { tags.removeAll(uuidsToRemove) } for (tag in CoreConfig.tagsDb.getAll()) { - options.add(LithoTagOptionsItem( + options.add( + LithoTagOptionsItem( tag = tag, listener = { onActionListener(tag, !tags.contains(tag.uuid)) @@ -83,7 +85,7 @@ class SelectedTagChooserBottomSheet : LithoBottomSheet() { reset(activity, dialog) }, isSelected = tags.contains(tag.uuid) - )) + )) } options.sortByDescending { if (it.isSelected) 1 else 0 } return options diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt index 7d16c423..bf44ffd2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/sheet/TagChooserBottomSheet.kt @@ -35,28 +35,29 @@ class TagChooserBottomSheet : LithoBottomSheet() { val activity = context as ThemedActivity val component = Column.create(componentContext) - .widthPercent(100f) + .widthPercent(100f) val tagsComponent = Column.create(componentContext) - .paddingDip(YogaEdge.TOP, 8f) - .paddingDip(YogaEdge.BOTTOM, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.tag_sheet_choose_tag) - .marginDip(YogaEdge.BOTTOM, 12f)) + .paddingDip(YogaEdge.TOP, 8f) + .paddingDip(YogaEdge.BOTTOM, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.tag_sheet_choose_tag) + .marginDip(YogaEdge.BOTTOM, 12f)) getTagOptions().forEach { tagsComponent.child(TagItemLayout.create(componentContext).option(it)) } val addTag = LithoOptionsItem( - title = R.string.tag_sheet_new_tag_button, - subtitle = 0, - icon = R.drawable.icon_add_note, - listener = { CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { _, _ -> reset(activity, dialog) } }) + title = R.string.tag_sheet_new_tag_button, + subtitle = 0, + icon = R.drawable.icon_add_note, + listener = { CreateOrEditTagBottomSheet.openSheet(activity, TagBuilder().emptyTag()) { _, _ -> reset(activity, dialog) } }) tagsComponent.child(OptionItemLayout.create(componentContext) - .option(addTag) - .backgroundRes(R.drawable.accent_rounded_bg) - .marginDip(YogaEdge.TOP, 16f) - .onClick { addTag.listener() }) + .option(addTag) + .backgroundRes(R.drawable.accent_rounded_bg) + .marginDip(YogaEdge.TOP, 16f) + .onClick { addTag.listener() }) component.child(tagsComponent) return component.build() @@ -67,7 +68,8 @@ class TagChooserBottomSheet : LithoBottomSheet() { val options = ArrayList() val tags = note!!.getTagUUIDs() for (tag in CoreConfig.tagsDb.getAll()) { - options.add(LithoTagOptionsItem( + options.add( + LithoTagOptionsItem( tag = tag, listener = { note!!.toggleTag(tag) @@ -75,7 +77,7 @@ class TagChooserBottomSheet : LithoBottomSheet() { reset(activity, dialog) }, isSelected = tags.contains(tag.uuid) - )) + )) } options.sortByDescending { if (it.isSelected) 1 else 0 } return options diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/HomeTagView.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/HomeTagView.kt index 3fd00e1c..92f4618a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/HomeTagView.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/HomeTagView.kt @@ -19,5 +19,4 @@ class HomeTagView(val rootView: View) { action = rootView.findViewById(R.id.action) } - } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt index b05c79b2..b3a6725d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt @@ -12,10 +12,10 @@ import com.maubis.scarlet.base.database.room.tag.Tag import com.maubis.scarlet.base.settings.view.ColorView class TagsAndColorPickerViewHolder( - val activity: MainActivity, - val flexbox: FlexboxLayout, - val onTagClick: (Tag) -> Unit, - val onColorClick: (Int) -> Unit) { + val activity: MainActivity, + val flexbox: FlexboxLayout, + val onTagClick: (Tag) -> Unit, + val onColorClick: (Int) -> Unit) { val tags = emptySet().toMutableSet() val colors = emptySet().toMutableSet() @@ -42,37 +42,37 @@ class TagsAndColorPickerViewHolder( private fun setTags() { val length = tags.size tags.toList() - .subList(0, Math.min(length, 6)) - .forEach { - val tag = it - val tagView = View.inflate(activity, R.layout.layout_flexbox_tag_item, null) as View - val text = tagView.findViewById(R.id.tag_text) + .subList(0, Math.min(length, 6)) + .forEach { + val tag = it + val tagView = View.inflate(activity, R.layout.layout_flexbox_tag_item, null) as View + val text = tagView.findViewById(R.id.tag_text) - if (activity.config.tags.filter { it.uuid == tag.uuid }.isNotEmpty()) { - text.setBackgroundResource(R.drawable.flexbox_selected_tag_item_bg) - text.setTextColor(ContextCompat.getColor(activity, R.color.colorAccent)) - } + if (activity.config.tags.filter { it.uuid == tag.uuid }.isNotEmpty()) { + text.setBackgroundResource(R.drawable.flexbox_selected_tag_item_bg) + text.setTextColor(ContextCompat.getColor(activity, R.color.colorAccent)) + } - text.text = it.title - tagView.setOnClickListener { - onTagClick(tag) - } - flexbox.addView(tagView) + text.text = it.title + tagView.setOnClickListener { + onTagClick(tag) } + flexbox.addView(tagView) + } } private fun setColors() { val length = colors.size colors.toList() - .subList(0, Math.min(length, 6)) - .forEach { - val color = it - val colorView = ColorView(activity, R.layout.layout_color_small) - colorView.setColor(color, activity.config.colors.contains(color)) - colorView.setOnClickListener { - onColorClick(color) - } - flexbox.addView(colorView) + .subList(0, Math.min(length, 6)) + .forEach { + val color = it + val colorView = ColorView(activity, R.layout.layout_color_small) + colorView.setColor(color, activity.config.colors.contains(color)) + colorView.setOnClickListener { + onColorClick(color) } + flexbox.addView(colorView) + } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt index ed25f0bc..de21ac4c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt +++ b/base/src/main/java/com/maubis/scarlet/base/notification/NotificationHandler.kt @@ -1,6 +1,10 @@ package com.maubis.scarlet.base.notification -import android.app.* +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.TaskStackBuilder import android.content.Context import android.content.Context.NOTIFICATION_SERVICE import android.content.Intent @@ -11,7 +15,6 @@ import android.widget.RemoteViews import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity @@ -30,8 +33,8 @@ const val NOTE_NOTIFICATION_CHANNEL_ID = "NOTE_NOTIFICATION_CHANNEL"; const val REMINDER_NOTIFICATION_CHANNEL_ID = "REMINDER_NOTIFICATION_CHANNEL"; class NotificationConfig( - val note: Note, - val channel: String = NOTE_NOTIFICATION_CHANNEL_ID + val note: Note, + val channel: String = NOTE_NOTIFICATION_CHANNEL_ID ) class NotificationHandler(val context: Context) { @@ -46,14 +49,14 @@ class NotificationHandler(val context: Context) { val pendingIntent = getPendingActivityIntent(config, getNoteOpenIntent(config), 1) var contentView = getRemoteView(config) val notificationBuilder = NotificationCompat.Builder(context, config.channel) - .setSmallIcon(R.drawable.ic_format_quote_white_48dp) - .setContentTitle(config.note.getTitleForSharing()) - .setColor(config.note.color) - .setCategory(NotificationCompat.CATEGORY_EVENT) - .setContent(contentView) - .setCustomBigContentView(contentView) - .setContentIntent(pendingIntent) - .setAutoCancel(false) + .setSmallIcon(R.drawable.ic_format_quote_white_48dp) + .setContentTitle(config.note.getTitleForSharing()) + .setColor(config.note.color) + .setCategory(NotificationCompat.CATEGORY_EVENT) + .setContent(contentView) + .setCustomBigContentView(contentView) + .setContentIntent(pendingIntent) + .setAutoCancel(false) if (config.channel === REMINDER_NOTIFICATION_CHANNEL_ID) { notificationBuilder.setPriority(NotificationCompat.PRIORITY_HIGH) @@ -76,15 +79,15 @@ class NotificationHandler(val context: Context) { } val channel = NotificationChannel( - NOTE_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_channel_label), - NotificationManager.IMPORTANCE_MIN) + NOTE_NOTIFICATION_CHANNEL_ID, + context.getString(R.string.notification_channel_label), + NotificationManager.IMPORTANCE_MIN) manager.createNotificationChannel(channel) val channelForReminder = NotificationChannel( - REMINDER_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_reminder_channel_label), - NotificationManager.IMPORTANCE_HIGH) + REMINDER_NOTIFICATION_CHANNEL_ID, + context.getString(R.string.notification_reminder_channel_label), + NotificationManager.IMPORTANCE_HIGH) manager.createNotificationChannel(channelForReminder) } @@ -113,20 +116,20 @@ class NotificationHandler(val context: Context) { contentView.setInt(R.id.edit_button, "setColorFilter", iconColor) contentView.setOnClickPendingIntent( - R.id.options_button, - getPendingActivityIntent(config, getNoteOpenIntent(config), 2)) + R.id.options_button, + getPendingActivityIntent(config, getNoteOpenIntent(config), 2)) contentView.setOnClickPendingIntent( - R.id.edit_button, - getPendingActivityIntent(config, getNoteEditIntent(config), 3)) + R.id.edit_button, + getPendingActivityIntent(config, getNoteEditIntent(config), 3)) contentView.setOnClickPendingIntent( - R.id.copy_button, - getPendingServiceIntent(config, getNoteActionIntent(config, NotificationIntentService.NoteAction.COPY), 4)) + R.id.copy_button, + getPendingServiceIntent(config, getNoteActionIntent(config, NotificationIntentService.NoteAction.COPY), 4)) contentView.setOnClickPendingIntent( - R.id.share_button, - getPendingServiceIntent(config, getNoteActionIntent(config, NotificationIntentService.NoteAction.SHARE), 5)) + R.id.share_button, + getPendingServiceIntent(config, getNoteActionIntent(config, NotificationIntentService.NoteAction.SHARE), 5)) contentView.setOnClickPendingIntent( - R.id.delete_button, - getPendingServiceIntent(config, getNoteActionIntent(config, NotificationIntentService.NoteAction.DELETE), 6)) + R.id.delete_button, + getPendingServiceIntent(config, getNoteActionIntent(config, NotificationIntentService.NoteAction.DELETE), 6)) return contentView } @@ -144,20 +147,20 @@ class NotificationHandler(val context: Context) { } private fun getPendingActivityIntent( - config: NotificationConfig, - intent: Intent, - requestCode: Int): PendingIntent { + config: NotificationConfig, + intent: Intent, + requestCode: Int): PendingIntent { val stackBuilder = TaskStackBuilder.create(context) stackBuilder.addParentStack(MainActivity::class.java) stackBuilder.addNextIntent(intent) return stackBuilder.getPendingIntent( - REQUEST_CODE_BASE + config.note.uid + requestCode * REQUEST_CODE_MULTIPLIER, - PendingIntent.FLAG_UPDATE_CURRENT) + REQUEST_CODE_BASE + config.note.uid + requestCode * REQUEST_CODE_MULTIPLIER, + PendingIntent.FLAG_UPDATE_CURRENT) } private fun getNoteActionIntent( - config: NotificationConfig, - action: NotificationIntentService.NoteAction): Intent { + config: NotificationConfig, + action: NotificationIntentService.NoteAction): Intent { val intent = Intent(context, NotificationIntentService::class.java) intent.putExtra(INTENT_KEY_NOTE_ID, config.note.uid) intent.putExtra(INTENT_KEY_ACTION, action.name) @@ -165,13 +168,13 @@ class NotificationHandler(val context: Context) { } private fun getPendingServiceIntent( - config: NotificationConfig, - intent: Intent, - requestCode: Int): PendingIntent { + config: NotificationConfig, + intent: Intent, + requestCode: Int): PendingIntent { return PendingIntent.getService( - context, - REQUEST_CODE_BASE + config.note.uid + requestCode * REQUEST_CODE_MULTIPLIER, - intent, - PendingIntent.FLAG_UPDATE_CURRENT) + context, + REQUEST_CODE_BASE + config.note.uid + requestCode * REQUEST_CODE_MULTIPLIER, + intent, + PendingIntent.FLAG_UPDATE_CURRENT) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt index df079621..40464264 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivity.kt @@ -28,19 +28,20 @@ class AppLockActivity : ThemedActivity() { setView() notifyThemeChange() } + private fun setView() { component = AppLockView.create(componentContext) - .fingerprintEnabled(Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled) - .onTextChange { text -> - passCodeEntered = text - } - .onClick { - if (passCodeEntered.length == 4 && sSecurityCode == passCodeEntered) { - PinLockController.notifyPinVerified() - finish() - } + .fingerprintEnabled(Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled) + .onTextChange { text -> + passCodeEntered = text + } + .onClick { + if (passCodeEntered.length == 4 && sSecurityCode == passCodeEntered) { + PinLockController.notifyPinVerified() + finish() } - .build() + } + .build() setContentView(LithoView.create(componentContext, component)) } @@ -54,15 +55,16 @@ class AppLockActivity : ThemedActivity() { } override fun onFailure( - failureReason: AuthenticationFailureReason?, - fatal: Boolean, - errorMessage: CharSequence?, - moduleTag: Int, - errorCode: Int) { + failureReason: AuthenticationFailureReason?, + fatal: Boolean, + errorMessage: CharSequence?, + moduleTag: Int, + errorCode: Int) { // Ignore } }) } + override fun onPause() { super.onPause() Reprint.cancelAuthentication() diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt index 1c440d19..98f7747c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -3,8 +3,16 @@ package com.maubis.scarlet.base.security.activity import android.text.InputType import android.text.Layout import android.view.inputmethod.EditorInfo -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row +import com.facebook.litho.annotations.FromEvent +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop import com.facebook.litho.widget.EditText import com.facebook.litho.widget.Image import com.facebook.litho.widget.Text @@ -12,7 +20,6 @@ import com.facebook.litho.widget.TextChangedEvent import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.EmptySpec @@ -23,42 +30,46 @@ import com.maubis.scarlet.base.support.utils.getEditorActionListener object AppLockViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop fingerprintEnabled: Boolean, - @Prop onTextChange: (String) -> Unit, - @Prop onClick: () -> Unit): Component { + fun onCreate( + context: ComponentContext, + @Prop fingerprintEnabled: Boolean, + @Prop onTextChange: (String) -> Unit, + @Prop onClick: () -> Unit): Component { return Column.create(context) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(AppLockContentView.create(context) - .fingerprintEnabled(fingerprintEnabled) - .onTextChange(onTextChange) - .onClick(onClick) - .flexGrow(1f)) - .child(Row.create(context) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .marginDip(YogaEdge.ALL, 16f) - .child( - when { - fingerprintEnabled -> Image.create(context) - .drawableRes(R.drawable.ic_option_fingerprint) - .heightDip(36f) - else -> null - } - ) - .child(EmptySpec.create(context).flexGrow(1f)) - .child(Text.create(context) - .backgroundRes(R.drawable.accent_rounded_bg) - .textSizeRes(R.dimen.font_size_large) - .textColorRes(R.color.white) - .textRes(R.string.security_sheet_button_unlock) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .paddingDip(YogaEdge.VERTICAL, 12f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .typeface(CoreConfig.FONT_MONSERRAT) - .clickHandler(AppLockView.onUnlockClick(context)))) - .build() + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + AppLockContentView.create(context) + .fingerprintEnabled(fingerprintEnabled) + .onTextChange(onTextChange) + .onClick(onClick) + .flexGrow(1f)) + .child( + Row.create(context) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + when { + fingerprintEnabled -> Image.create(context) + .drawableRes(R.drawable.ic_option_fingerprint) + .heightDip(36f) + else -> null + } + ) + .child(EmptySpec.create(context).flexGrow(1f)) + .child( + Text.create(context) + .backgroundRes(R.drawable.accent_rounded_bg) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(R.string.security_sheet_button_unlock) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .typeface(CoreConfig.FONT_MONSERRAT) + .clickHandler(AppLockView.onUnlockClick(context)))) + .build() } @OnEvent(ClickEvent::class) @@ -71,9 +82,10 @@ object AppLockViewSpec { object AppLockContentViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop fingerprintEnabled: Boolean, - @Prop onClick: () -> Unit): Component { + fun onCreate( + context: ComponentContext, + @Prop fingerprintEnabled: Boolean, + @Prop onClick: () -> Unit): Component { val description = when { fingerprintEnabled -> R.string.app_lock_details else -> R.string.app_lock_details_no_fingerprint @@ -84,40 +96,43 @@ object AppLockContentViewSpec { } return Column.create(context) - .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_xxlarge) - .textRes(R.string.app_lock_title) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .textRes(description) - .typeface(CoreConfig.FONT_MONSERRAT)) - .child(EmptySpec.create(context).flexGrow(1f)) - .child(EditText.create(context) - .backgroundRes(editBackground) - .textSizeRes(R.dimen.font_size_xlarge) - .minWidthDip(128f) - .maxLength(4) - .hint("****") - .alignSelf(YogaAlign.CENTER) - .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) - .paddingDip(YogaEdge.HORIZONTAL, 22f) - .paddingDip(YogaEdge.VERTICAL, 6f) - .imeOptions(EditorInfo.IME_ACTION_DONE) - .editorActionListener(getEditorActionListener({ - onClick() - true - })) - .textChangedEventHandler(AppLockContentView.onTextChanged(context))) - .child(EmptySpec.create(context).flexGrow(1f)) - .build() + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.app_lock_title) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textRes(description) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child(EmptySpec.create(context).flexGrow(1f)) + .child( + EditText.create(context) + .backgroundRes(editBackground) + .textSizeRes(R.dimen.font_size_xlarge) + .minWidthDip(128f) + .maxLength(4) + .hint("****") + .alignSelf(YogaAlign.CENTER) + .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_OPEN_SANS) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .paddingDip(YogaEdge.HORIZONTAL, 22f) + .paddingDip(YogaEdge.VERTICAL, 6f) + .imeOptions(EditorInfo.IME_ACTION_DONE) + .editorActionListener(getEditorActionListener({ + onClick() + true + })) + .textChangedEventHandler(AppLockContentView.onTextChanged(context))) + .child(EmptySpec.create(context).flexGrow(1f)) + .build() } @OnEvent(TextChangedEvent::class) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt index 96198ccc..aa79321b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt @@ -7,7 +7,6 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -16,7 +15,6 @@ import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedActivity - const val STORE_KEY_NO_PIN_ASK = "KEY_NO_PIN_ASK" var sNoPinSetupNoticeShown: Boolean get() = sAppPreferences.get(STORE_KEY_NO_PIN_ASK, false) @@ -28,36 +26,38 @@ class NoPincodeBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val activity = context as ThemedActivity val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.no_pincode_sheet_title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .textRes(R.string.no_pincode_sheet_details) - .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.no_pincode_sheet_set_up) - .onPrimaryClick { - openCreateSheet( - activity = activity, - onCreateSuccess = {}) - dismiss() - } - .secondaryActionRes(R.string.no_pincode_sheet_dont_ask) - .onSecondaryClick { - onSuccess() - dismiss() - } - .tertiaryActionRes(R.string.no_pincode_sheet_not_now) - .onTertiaryClick { - onSuccess() - dismiss() - } - .paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.no_pincode_sheet_title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .textRes(R.string.no_pincode_sheet_details) + .marginDip(YogaEdge.BOTTOM, 16f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.no_pincode_sheet_set_up) + .onPrimaryClick { + openCreateSheet( + activity = activity, + onCreateSuccess = {}) + dismiss() + } + .secondaryActionRes(R.string.no_pincode_sheet_dont_ask) + .onSecondaryClick { + onSuccess() + dismiss() + } + .tertiaryActionRes(R.string.no_pincode_sheet_not_now) + .onTertiaryClick { + onSuccess() + dismiss() + } + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index c831f93c..1ce29ec9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -4,8 +4,16 @@ import android.app.Dialog import android.text.InputType import android.text.Layout import android.view.inputmethod.EditorInfo -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row +import com.facebook.litho.annotations.FromEvent +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop import com.facebook.litho.widget.EditText import com.facebook.litho.widget.Image import com.facebook.litho.widget.Text @@ -17,7 +25,6 @@ import com.github.ajalt.reprint.core.AuthenticationListener import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.security.controller.PinLockController @@ -35,22 +42,22 @@ import com.maubis.scarlet.base.support.ui.ThemedActivity import com.maubis.scarlet.base.support.utils.getEditorActionListener data class PincodeSheetData( - val title: Int, - val actionTitle: Int, - val onSuccess: () -> Unit, - val onFailure: () -> Unit = {}, - val isFingerprintEnabled: Boolean = false, - val onActionClicked: (String) -> Unit = { password -> - when { - password != "" && password == sSecurityCode -> { - PinLockController.notifyPinVerified() - onSuccess() - } - else -> onFailure() + val title: Int, + val actionTitle: Int, + val onSuccess: () -> Unit, + val onFailure: () -> Unit = {}, + val isFingerprintEnabled: Boolean = false, + val onActionClicked: (String) -> Unit = { password -> + when { + password != "" && password == sSecurityCode -> { + PinLockController.notifyPinVerified() + onSuccess() } - }, - val isRemoveButtonEnabled: Boolean = false, - val onRemoveButtonClick: () -> Unit = {}) + else -> onFailure() + } + }, + val isRemoveButtonEnabled: Boolean = false, + val onRemoveButtonClick: () -> Unit = {}) @LayoutSpec object PincodeSheetViewSpec { @@ -58,84 +65,90 @@ object PincodeSheetViewSpec { private var passcodeEntered = "" @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop data: PincodeSheetData, - @Prop dismiss: () -> Unit): Component { + fun onCreate( + context: ComponentContext, + @Prop data: PincodeSheetData, + @Prop dismiss: () -> Unit): Component { val editBackground = when { sAppTheme.isNightTheme() -> R.drawable.light_secondary_rounded_bg else -> R.drawable.secondary_rounded_bg } val component = Column.create(context) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(context) - .textRes(data.title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textRes(R.string.app_lock_details) - .marginDip(YogaEdge.BOTTOM, 16f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(EditText.create(context) - .backgroundRes(editBackground) - .textSizeRes(R.dimen.font_size_xlarge) - .minWidthDip(128f) - .maxLength(4) - .alignSelf(YogaAlign.CENTER) - .hint("****") - .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) - .paddingDip(YogaEdge.HORIZONTAL, 22f) - .paddingDip(YogaEdge.VERTICAL, 6f) - .marginDip(YogaEdge.VERTICAL, 8f) - .imeOptions(EditorInfo.IME_ACTION_DONE) - .editorActionListener(getEditorActionListener({ - data.onActionClicked(passcodeEntered) - dismiss() - true - })) - .textChangedEventHandler(PincodeSheetView.onTextChangeListener(context))) - .child(Row.create(context) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 8f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child( - when { - data.isFingerprintEnabled -> Image.create(context) - .drawableRes(R.drawable.ic_option_fingerprint) - .heightDip(36f) - else -> null - } - ) - .child( - when { - data.isRemoveButtonEnabled -> Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.HINT_TEXT)) - .textRes(R.string.security_sheet_button_remove) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .paddingDip(YogaEdge.VERTICAL, 12f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .typeface(CoreConfig.FONT_MONSERRAT) - .clickHandler(PincodeSheetView.onRemoveClick(context)) - else -> null - } - ) - .child(EmptySpec.create(context).flexGrow(1f)) - .child(Text.create(context) - .backgroundRes(R.drawable.accent_rounded_bg) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(context) + .textRes(data.title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textRes(R.string.app_lock_details) + .marginDip(YogaEdge.BOTTOM, 16f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child( + EditText.create(context) + .backgroundRes(editBackground) + .textSizeRes(R.dimen.font_size_xlarge) + .minWidthDip(128f) + .maxLength(4) + .alignSelf(YogaAlign.CENTER) + .hint("****") + .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_OPEN_SANS) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .paddingDip(YogaEdge.HORIZONTAL, 22f) + .paddingDip(YogaEdge.VERTICAL, 6f) + .marginDip(YogaEdge.VERTICAL, 8f) + .imeOptions(EditorInfo.IME_ACTION_DONE) + .editorActionListener(getEditorActionListener({ + data.onActionClicked(passcodeEntered) + dismiss() + true + })) + .textChangedEventHandler(PincodeSheetView.onTextChangeListener(context))) + .child( + Row.create(context) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 8f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child( + when { + data.isFingerprintEnabled -> Image.create(context) + .drawableRes(R.drawable.ic_option_fingerprint) + .heightDip(36f) + else -> null + } + ) + .child( + when { + data.isRemoveButtonEnabled -> Text.create(context) .textSizeRes(R.dimen.font_size_large) - .textColorRes(R.color.white) - .textRes(data.actionTitle) + .textColor(sAppTheme.get(ThemeColorType.HINT_TEXT)) + .textRes(R.string.security_sheet_button_remove) .textAlignment(Layout.Alignment.ALIGN_CENTER) .paddingDip(YogaEdge.VERTICAL, 12f) .paddingDip(YogaEdge.HORIZONTAL, 20f) .typeface(CoreConfig.FONT_MONSERRAT) - .clickHandler(PincodeSheetView.onActionClick(context)))) + .clickHandler(PincodeSheetView.onRemoveClick(context)) + else -> null + } + ) + .child(EmptySpec.create(context).flexGrow(1f)) + .child( + Text.create(context) + .backgroundRes(R.drawable.accent_rounded_bg) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(data.actionTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .typeface(CoreConfig.FONT_MONSERRAT) + .clickHandler(PincodeSheetView.onActionClick(context)))) return component.build() } @@ -145,18 +158,20 @@ object PincodeSheetViewSpec { } @OnEvent(ClickEvent::class) - fun onActionClick(context: ComponentContext, - @Prop data: PincodeSheetData, - @Prop dismiss: () -> Unit) { + fun onActionClick( + context: ComponentContext, + @Prop data: PincodeSheetData, + @Prop dismiss: () -> Unit) { data.onActionClicked(passcodeEntered) passcodeEntered = "" dismiss() } @OnEvent(ClickEvent::class) - fun onRemoveClick(context: ComponentContext, - @Prop data: PincodeSheetData, - @Prop dismiss: () -> Unit) { + fun onRemoveClick( + context: ComponentContext, + @Prop data: PincodeSheetData, + @Prop dismiss: () -> Unit) { data.onRemoveButtonClick() passcodeEntered = "" dismiss() @@ -165,15 +180,15 @@ object PincodeSheetViewSpec { class PincodeBottomSheet : LithoBottomSheet() { var data = PincodeSheetData( - title = R.string.no_pincode_sheet_title, - actionTitle = R.string.no_pincode_sheet_details, - onSuccess = {}) + title = R.string.no_pincode_sheet_title, + actionTitle = R.string.no_pincode_sheet_details, + onSuccess = {}) override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { return PincodeSheetView.create(componentContext) - .data(data) - .dismiss { dismiss() } - .build() + .data(data) + .dismiss { dismiss() } + .build() } override fun onResume() { @@ -185,7 +200,8 @@ class PincodeBottomSheet : LithoBottomSheet() { dismiss() } - override fun onFailure(failureReason: AuthenticationFailureReason?, fatal: Boolean, errorMessage: CharSequence?, moduleTag: Int, errorCode: Int) { + override fun onFailure( + failureReason: AuthenticationFailureReason?, fatal: Boolean, errorMessage: CharSequence?, moduleTag: Int, errorCode: Int) { } }) } @@ -200,55 +216,55 @@ class PincodeBottomSheet : LithoBottomSheet() { } fun openCreateSheet( - activity: ThemedActivity, - onCreateSuccess: () -> Unit) { + activity: ThemedActivity, + onCreateSuccess: () -> Unit) { openSheet(activity, PincodeBottomSheet().apply { data = PincodeSheetData( - title = R.string.security_sheet_enter_new_pin_title, - actionTitle = R.string.security_sheet_button_set, - isFingerprintEnabled = false, - isRemoveButtonEnabled = true, - onRemoveButtonClick = { - sSecurityCode = "" - sSecurityAppLockEnabled = false - sNoPinSetupNoticeShown = false - onCreateSuccess() + title = R.string.security_sheet_enter_new_pin_title, + actionTitle = R.string.security_sheet_button_set, + isFingerprintEnabled = false, + isRemoveButtonEnabled = true, + onRemoveButtonClick = { + sSecurityCode = "" + sSecurityAppLockEnabled = false + sNoPinSetupNoticeShown = false + onCreateSuccess() - if (activity is MainActivity) { - activity.setupData() - } - }, - onActionClicked = { password: String -> - if (password.length == 4 && password.toIntOrNull() !== null) { - sSecurityCode = password - onCreateSuccess() - } - }, - onSuccess = {} + if (activity is MainActivity) { + activity.setupData() + } + }, + onActionClicked = { password: String -> + if (password.length == 4 && password.toIntOrNull() !== null) { + sSecurityCode = password + onCreateSuccess() + } + }, + onSuccess = {} ) }) } fun openVerifySheet( - activity: ThemedActivity, - onVerifySuccess: () -> Unit, - onVerifyFailure: () -> Unit = {}) { + activity: ThemedActivity, + onVerifySuccess: () -> Unit, + onVerifyFailure: () -> Unit = {}) { openSheet(activity, PincodeBottomSheet().apply { data = PincodeSheetData( - title = R.string.security_sheet_enter_current_pin_title, - actionTitle = R.string.security_sheet_button_verify, - onSuccess = onVerifySuccess, - onFailure = onVerifyFailure, - isFingerprintEnabled = Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled + title = R.string.security_sheet_enter_current_pin_title, + actionTitle = R.string.security_sheet_button_verify, + onSuccess = onVerifySuccess, + onFailure = onVerifyFailure, + isFingerprintEnabled = Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled ) }) } fun openUnlockSheet( - activity: ThemedActivity, - onUnlockSuccess: () -> Unit, - onUnlockFailure: () -> Unit) { + activity: ThemedActivity, + onUnlockSuccess: () -> Unit, + onUnlockFailure: () -> Unit) { if (!isPinCodeEnabled()) { if (sNoPinSetupNoticeShown) { onUnlockSuccess() @@ -265,11 +281,11 @@ fun openUnlockSheet( } openSheet(activity, PincodeBottomSheet().apply { data = PincodeSheetData( - title = R.string.security_sheet_enter_pin_to_unlock_title, - actionTitle = R.string.security_sheet_button_unlock, - onSuccess = onUnlockSuccess, - onFailure = onUnlockFailure, - isFingerprintEnabled = Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled + title = R.string.security_sheet_enter_pin_to_unlock_title, + actionTitle = R.string.security_sheet_button_unlock, + onSuccess = onUnlockSuccess, + onFailure = onUnlockFailure, + isFingerprintEnabled = Reprint.hasFingerprintRegistered() && sSecurityFingerprintEnabled ) }) } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt index 7a44ed34..a66aae0d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/FloatingNoteService.kt @@ -13,14 +13,17 @@ import com.bsk.floatingbubblelib.FloatingBubblePermissions import com.bsk.floatingbubblelib.FloatingBubbleService import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.database.room.note.Note -import com.maubis.scarlet.base.note.* +import com.maubis.scarlet.base.note.copy import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity import com.maubis.scarlet.base.note.creation.activity.INTENT_KEY_NOTE_ID +import com.maubis.scarlet.base.note.getDisplayTime +import com.maubis.scarlet.base.note.getFullTextForDirectMarkdownRender +import com.maubis.scarlet.base.note.getTextForSharing +import com.maubis.scarlet.base.note.getTitleForSharing import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.utils.maybeThrow @@ -38,21 +41,22 @@ class FloatingNoteService : FloatingBubbleService() { override fun getConfig(): FloatingBubbleConfig { return FloatingBubbleConfig.Builder() - .bubbleIcon(ContextCompat.getDrawable(context, R.drawable.app_icon)) - .removeBubbleIcon(ContextCompat.getDrawable( - context, - com.bsk.floatingbubblelib.R.drawable.close_default_icon)) - .bubbleIconDp(72) - .removeBubbleIconDp(72) - .paddingDp(8) - .borderRadiusDp(4) - .physicsEnabled(true) - .expandableColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .triangleColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .gravity(Gravity.END) - .expandableView(loadView()) - .removeBubbleAlpha(0.7f) - .build() + .bubbleIcon(ContextCompat.getDrawable(context, R.drawable.app_icon)) + .removeBubbleIcon( + ContextCompat.getDrawable( + context, + com.bsk.floatingbubblelib.R.drawable.close_default_icon)) + .bubbleIconDp(72) + .removeBubbleIconDp(72) + .paddingDp(8) + .borderRadiusDp(4) + .physicsEnabled(true) + .expandableColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .triangleColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .gravity(Gravity.END) + .expandableView(loadView()) + .removeBubbleAlpha(0.7f) + .build() } override fun onGetIntent(intent: Intent): Boolean { diff --git a/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt b/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt index 48243199..94c6589b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt +++ b/base/src/main/java/com/maubis/scarlet/base/service/SyncedNoteBroadcastReceiver.kt @@ -29,9 +29,9 @@ fun getNoteIntentFilter(): IntentFilter { } fun sendNoteBroadcast( - context: Context, - broadcast: NoteBroadcast, - uuid: String) { + context: Context, + broadcast: NoteBroadcast, + uuid: String) { val intent = Intent() intent.action = broadcast.name intent.putExtra(KEY_UUID, uuid) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt index 5de0a078..972e21f0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutSettingsOptionsBottomSheet.kt @@ -22,66 +22,68 @@ class AboutSettingsOptionsBottomSheet : LithoOptionBottomSheet() { val activity = context as MainActivity val options = ArrayList() options.add(LithoOptionsItem( - title = R.string.home_option_about_page, - subtitle = R.string.home_option_about_page_subtitle, - icon = R.drawable.ic_info, - listener = { - openSheet(activity, AboutUsBottomSheet()) - dismiss() - } - )) - options.add(LithoOptionsItem( - title = R.string.home_option_open_source_page, - subtitle = R.string.home_option_open_source_page_subtitle, - icon = R.drawable.ic_code_white_48dp, - listener = { - openSheet(activity, OpenSourceBottomSheet()) - dismiss() - } + title = R.string.home_option_about_page, + subtitle = R.string.home_option_about_page_subtitle, + icon = R.drawable.ic_info, + listener = { + openSheet(activity, AboutUsBottomSheet()) + dismiss() + } )) options.add(LithoOptionsItem( - title = R.string.home_option_faq_title, - subtitle = R.string.home_option_faq_description, - icon = R.drawable.icon_help, - listener = { - try { - activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SettingsOptionsBottomSheet.GITHUB_FAQ_URL))) - dismiss() - } catch (exception: Exception) { - maybeThrow(activity, exception) - } - } + title = R.string.home_option_open_source_page, + subtitle = R.string.home_option_open_source_page_subtitle, + icon = R.drawable.ic_code_white_48dp, + listener = { + openSheet(activity, OpenSourceBottomSheet()) + dismiss() + } )) options.add(LithoOptionsItem( - title = R.string.whats_new_title, - subtitle = R.string.whats_new_subtitle, - icon = R.drawable.ic_whats_new, - listener = { - openSheet(activity, WhatsNewBottomSheet()) + title = R.string.home_option_faq_title, + subtitle = R.string.home_option_faq_description, + icon = R.drawable.icon_help, + listener = { + try { + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SettingsOptionsBottomSheet.GITHUB_FAQ_URL))) dismiss() + } catch (exception: Exception) { + maybeThrow(activity, exception) } + } )) options.add(LithoOptionsItem( + title = R.string.whats_new_title, + subtitle = R.string.whats_new_subtitle, + icon = R.drawable.ic_whats_new, + listener = { + openSheet(activity, WhatsNewBottomSheet()) + dismiss() + } + )) + options.add( + LithoOptionsItem( title = R.string.material_notes_privacy_policy, subtitle = R.string.material_notes_privacy_policy_subtitle, icon = R.drawable.ic_privacy_policy, listener = { - activity.startActivity(Intent( + activity.startActivity( + Intent( Intent.ACTION_VIEW, Uri.parse(PRIVACY_POLICY_LINK))) dismiss() }, visible = FlavorUtils.isPlayStore() - )) + )) options.add(LithoOptionsItem( - title = R.string.internal_settings_title, - subtitle = R.string.internal_settings_description, - icon = R.drawable.icon_code_block, - listener = { - openSheet(activity, InternalSettingsOptionsBottomSheet()) - dismiss() - } + title = R.string.internal_settings_title, + subtitle = R.string.internal_settings_description, + icon = R.drawable.icon_code_block, + listener = { + openSheet(activity, InternalSettingsOptionsBottomSheet()) + dismiss() + } )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index 7075d712..e6c4ac2e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -9,7 +9,6 @@ import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -36,49 +35,55 @@ class AboutUsBottomSheet : LithoBottomSheet() { val aboutAppDetails = getString(R.string.about_page_description, appName) val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.home_option_about_page) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .text(aboutUsDetails) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_xlarge) - .marginDip(YogaEdge.BOTTOM, 4f) - .textRes(R.string.about_page_about_app) - .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .text(aboutAppDetails) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_xlarge) - .marginDip(YogaEdge.BOTTOM, 4f) - .textRes(R.string.about_page_app_version) - .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .text(version) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.about_page_rate) - .onPrimaryClick { - try { - IntentUtils.openAppPlayStore(activity) - dismiss() - } catch (exception: Exception) { - maybeThrow(activity, exception) - } - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.home_option_about_page) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .text(aboutUsDetails) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_xlarge) + .marginDip(YogaEdge.BOTTOM, 4f) + .textRes(R.string.about_page_about_app) + .typeface(CoreConfig.FONT_MONSERRAT) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .text(aboutAppDetails) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_xlarge) + .marginDip(YogaEdge.BOTTOM, 4f) + .textRes(R.string.about_page_app_version) + .typeface(CoreConfig.FONT_MONSERRAT) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .text(version) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.about_page_rate) + .onPrimaryClick { + try { + IntentUtils.openAppPlayStore(activity) + dismiss() + } catch (exception: Exception) { + maybeThrow(activity, exception) + } + }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt index a5e316a0..4891b45b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ColorPickerBottomSheet.kt @@ -2,7 +2,11 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog import android.graphics.Color -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -19,33 +23,35 @@ import com.maubis.scarlet.base.support.specs.separatorSpec import com.maubis.scarlet.base.support.ui.ColorUtil class ColorPickerDefaultController( - val title: Int = R.string.note_option_default_color, - var selectedColor: Int = Color.WHITE, - val colors: List = listOf(intArrayOf(Color.WHITE)), - val onColorSelected: (Int) -> Unit = {}, - val columns: Int = 6) + val title: Int = R.string.note_option_default_color, + var selectedColor: Int = Color.WHITE, + val colors: List = listOf(intArrayOf(Color.WHITE)), + val onColorSelected: (Int) -> Unit = {}, + val columns: Int = 6) @LayoutSpec object ColorPickerItemSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop color: Int, - @Prop isSelected: Boolean): Component { + fun onCreate( + context: ComponentContext, + @Prop color: Int, + @Prop isSelected: Boolean): Component { val row = Row.create(context) - .alignItems(YogaAlign.CENTER) - .child(RoundIcon.create(context) - .iconRes(when { - isSelected -> R.drawable.ic_done_white_48dp - color == Color.TRANSPARENT -> R.drawable.icon_no_color - else -> R.drawable.ic_empty - }) - .bgColor(color) - .showBorder(true) - .iconColorRes(if (ColorUtil.isLightColored(color)) R.color.dark_tertiary_text else R.color.light_secondary_text) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .onClick { } - .isClickDisabled(true)) + .alignItems(YogaAlign.CENTER) + .child(RoundIcon.create(context) + .iconRes( + when { + isSelected -> R.drawable.ic_done_white_48dp + color == Color.TRANSPARENT -> R.drawable.icon_no_color + else -> R.drawable.ic_empty + }) + .bgColor(color) + .showBorder(true) + .iconColorRes(if (ColorUtil.isLightColored(color)) R.color.dark_tertiary_text else R.color.light_secondary_text) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .onClick { } + .isClickDisabled(true)) row.clickHandler(ColorPickerItem.onItemClick(context)) return row.build() } @@ -62,12 +68,13 @@ class ColorPickerBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val column = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(config.title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(config.title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) config.colors.forEachIndexed { colorArrayIndex, colorArray -> var flex: Row.Builder? = null @@ -75,21 +82,21 @@ class ColorPickerBottomSheet : LithoBottomSheet() { if (index % config.columns == 0) { column.child(flex) flex = Row.create(componentContext) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.VERTICAL, 8f) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 8f) } flex?.child( - ColorPickerItem.create(componentContext) - .color(color) - .isSelected(color == config.selectedColor) - .onColorSelected { selectedColor -> - config.selectedColor = selectedColor - config.onColorSelected(selectedColor) - reset(componentContext.androidContext, dialog) - } - .flexGrow(1f)) + ColorPickerItem.create(componentContext) + .color(color) + .isSelected(color == config.selectedColor) + .onColorSelected { selectedColor -> + config.selectedColor = selectedColor + config.onColorSelected(selectedColor) + reset(componentContext.androidContext, dialog) + } + .flexGrow(1f)) } column.child(flex) @@ -100,10 +107,10 @@ class ColorPickerBottomSheet : LithoBottomSheet() { } } column.child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.import_export_layout_exporting_done) - .onPrimaryClick { - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return column.build() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt index 06fdc0d9..6fe1dbc6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/DeleteAndMoreOptionsBottomSheet.kt @@ -14,7 +14,11 @@ import com.maubis.scarlet.base.note.folder.delete import com.maubis.scarlet.base.note.tag.delete import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class DeleteAndMoreOptionsBottomSheet : LithoOptionBottomSheet() { override fun title(): Int = R.string.home_option_delete_notes_and_more @@ -23,71 +27,72 @@ class DeleteAndMoreOptionsBottomSheet : LithoOptionBottomSheet() { val activity = context as MainActivity val options = ArrayList() options.add(LithoOptionsItem( - title = R.string.home_option_delete_all_notes, - subtitle = R.string.home_option_delete_all_notes_details, - icon = R.drawable.ic_note_white_48dp, - listener = { - openDeleteAllXSheet(activity, R.string.home_option_delete_all_notes_details) { - GlobalScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { notesDb.getAll().forEach { it.delete(activity) } } - activity.resetAndSetupData() - dismiss() - } + title = R.string.home_option_delete_all_notes, + subtitle = R.string.home_option_delete_all_notes_details, + icon = R.drawable.ic_note_white_48dp, + listener = { + openDeleteAllXSheet(activity, R.string.home_option_delete_all_notes_details) { + GlobalScope.launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { notesDb.getAll().forEach { it.delete(activity) } } + activity.resetAndSetupData() + dismiss() } } + } )) options.add(LithoOptionsItem( - title = R.string.home_option_delete_all_tags, - subtitle = R.string.home_option_delete_all_tags_details, - icon = R.drawable.ic_action_tags, - listener = { - openDeleteAllXSheet(activity, R.string.home_option_delete_all_tags_details) { - GlobalScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { tagsDb.getAll().forEach { it.delete() } } - activity.resetAndSetupData() - dismiss() - } + title = R.string.home_option_delete_all_tags, + subtitle = R.string.home_option_delete_all_tags_details, + icon = R.drawable.ic_action_tags, + listener = { + openDeleteAllXSheet(activity, R.string.home_option_delete_all_tags_details) { + GlobalScope.launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { tagsDb.getAll().forEach { it.delete() } } + activity.resetAndSetupData() + dismiss() } } + } )) options.add(LithoOptionsItem( - title = R.string.home_option_delete_all_folders, - subtitle = R.string.home_option_delete_all_folders_details, - icon = R.drawable.ic_folder, - listener = { - openDeleteAllXSheet(activity, R.string.home_option_delete_all_folders_details) { - GlobalScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { foldersDb.getAll().forEach { it.delete() } } - activity.resetAndSetupData() - dismiss() - } + title = R.string.home_option_delete_all_folders, + subtitle = R.string.home_option_delete_all_folders_details, + icon = R.drawable.ic_folder, + listener = { + openDeleteAllXSheet(activity, R.string.home_option_delete_all_folders_details) { + GlobalScope.launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { foldersDb.getAll().forEach { it.delete() } } + activity.resetAndSetupData() + dismiss() } } + } )) options.add(LithoOptionsItem( - title = R.string.home_option_delete_everything, - subtitle = R.string.home_option_delete_everything_details, - icon = R.drawable.ic_delete_permanently, - listener = { - openDeleteAllXSheet(activity, R.string.home_option_delete_everything_details) { - GlobalScope.launch(Dispatchers.Main) { - val notes = GlobalScope.async(Dispatchers.IO) { notesDb.getAll().forEach { it.delete(activity) } } - val tags = GlobalScope.async(Dispatchers.IO) { tagsDb.getAll().forEach { it.delete() } } - val folders = GlobalScope.async(Dispatchers.IO) { foldersDb.getAll().forEach { it.delete() } } + title = R.string.home_option_delete_everything, + subtitle = R.string.home_option_delete_everything_details, + icon = R.drawable.ic_delete_permanently, + listener = { + openDeleteAllXSheet(activity, R.string.home_option_delete_everything_details) { + GlobalScope.launch(Dispatchers.Main) { + val notes = GlobalScope.async(Dispatchers.IO) { notesDb.getAll().forEach { it.delete(activity) } } + val tags = GlobalScope.async(Dispatchers.IO) { tagsDb.getAll().forEach { it.delete() } } + val folders = GlobalScope.async(Dispatchers.IO) { foldersDb.getAll().forEach { it.delete() } } - notes.await() - tags.await() - folders.await() + notes.await() + tags.await() + folders.await() - activity.resetAndSetupData() - dismiss() - } + activity.resetAndSetupData() + dismiss() } - } + + } )) val forgetMeClick = ApplicationBase.instance.authenticator().openForgetMeActivity(activity) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.forget_me_option_title, subtitle = R.string.forget_me_option_details, icon = R.drawable.ic_action_forget_me, @@ -96,7 +101,7 @@ class DeleteAndMoreOptionsBottomSheet : LithoOptionBottomSheet() { dismiss() }, visible = forgetMeClick !== null && ApplicationBase.instance.authenticator().isLegacyLoggedIn() - )) + )) return options } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt index ac0e868b..d3a2a955 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt @@ -8,7 +8,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -31,31 +30,33 @@ class FontSizeBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val activity = context as MainActivity val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.note_option_font_size) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeDip(sEditorTextSize.toFloat()) - .marginDip(YogaEdge.BOTTOM, 16f) - .textRes(R.string.note_option_font_size_example) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(CounterChooser.create(componentContext) - .value(sEditorTextSize) - .minValue(TEXT_SIZE_MIN) - .maxValue(TEXT_SIZE_MAX) - .onValueChange { value -> - sEditorTextSize = value - reset(activity, dialog) - } - .paddingDip(YogaEdge.VERTICAL, 16f)) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.import_export_layout_exporting_done) - .onPrimaryClick { - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.note_option_font_size) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeDip(sEditorTextSize.toFloat()) + .marginDip(YogaEdge.BOTTOM, 16f) + .textRes(R.string.note_option_font_size_example) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(CounterChooser.create(componentContext) + .value(sEditorTextSize) + .minValue(TEXT_SIZE_MIN) + .maxValue(TEXT_SIZE_MAX) + .onValueChange { value -> + sEditorTextSize = value + reset(activity, dialog) + } + .paddingDip(YogaEdge.VERTICAL, 16f)) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt index 376eafbd..aafc51bf 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/InternalSettingsOptionsBottomSheet.kt @@ -7,7 +7,11 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.support.sheets.LithoOptionBottomSheet import com.maubis.scarlet.base.support.sheets.LithoOptionsItem -import com.maubis.scarlet.base.support.utils.* +import com.maubis.scarlet.base.support.utils.maybeThrow +import com.maubis.scarlet.base.support.utils.sInternalLogTracesToNote +import com.maubis.scarlet.base.support.utils.sInternalShowTracesInSheet +import com.maubis.scarlet.base.support.utils.sInternalThrowOnException +import com.maubis.scarlet.base.support.utils.sInternalThrownExceptionCount const val KEY_INTERNAL_ENABLE_FULL_SCREEN = "internal_enable_full_screen" var sInternalEnableFullScreen: Boolean @@ -25,7 +29,8 @@ class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = context as MainActivity val options = ArrayList() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.internal_settings_enable_fullscreen_title, subtitle = R.string.internal_settings_enable_fullscreen_description, icon = R.drawable.ic_action_grid, @@ -35,8 +40,9 @@ class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sInternalEnableFullScreen - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.internal_settings_show_uuid_title, subtitle = R.string.internal_settings_show_uuid_description, icon = R.drawable.ic_code_white_48dp, @@ -46,8 +52,9 @@ class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sInternalShowUUID - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.internal_settings_enable_log_exceptions_title, subtitle = R.string.internal_settings_enable_log_exceptions_description, icon = R.drawable.ic_note_white_48dp, @@ -57,8 +64,9 @@ class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sInternalLogTracesToNote - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.internal_settings_enable_show_exceptions_title, subtitle = R.string.internal_settings_enable_show_exceptions_description, icon = R.drawable.icon_add_list, @@ -68,8 +76,9 @@ class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sInternalShowTracesInSheet - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.internal_settings_enable_throw_exceptions_title, subtitle = R.string.internal_settings_enable_throw_exceptions_description, icon = R.drawable.ic_whats_new, @@ -80,14 +89,14 @@ class InternalSettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sInternalThrowOnException - )) + )) options.add(LithoOptionsItem( - title = R.string.internal_settings_fake_exceptions_title, - subtitle = R.string.internal_settings_fake_exceptions_description, - icon = R.drawable.ic_info, - listener = { - maybeThrow(activity, RuntimeException("Fake Exception for Testing")) - } + title = R.string.internal_settings_fake_exceptions_title, + subtitle = R.string.internal_settings_fake_exceptions_description, + icon = R.drawable.ic_info, + listener = { + maybeThrow(activity, RuntimeException("Fake Exception for Testing")) + } )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt index ac517b0b..72261eb0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/LineCountBottomSheet.kt @@ -30,27 +30,28 @@ class LineCountBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val activity = context as MainActivity val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.note_option_number_lines) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(CounterChooser.create(componentContext) - .value(sNoteItemLineCount) - .minValue(LINE_COUNT_MIN) - .maxValue(LINE_COUNT_MAX) - .onValueChange { value -> - sNoteItemLineCount = value - GlobalScope.launch(Dispatchers.Main) { reset(activity, dialog) } - GlobalScope.launch(Dispatchers.Main) { activity.notifyAdapterExtraChanged() } - } - .paddingDip(YogaEdge.VERTICAL, 16f)) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.import_export_layout_exporting_done) - .onPrimaryClick { - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.note_option_number_lines) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child(CounterChooser.create(componentContext) + .value(sNoteItemLineCount) + .minValue(LINE_COUNT_MIN) + .maxValue(LINE_COUNT_MAX) + .onValueChange { value -> + sNoteItemLineCount = value + GlobalScope.launch(Dispatchers.Main) { reset(activity, dialog) } + GlobalScope.launch(Dispatchers.Main) { activity.notifyAdapterExtraChanged() } + } + .paddingDip(YogaEdge.VERTICAL, 16f)) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 2480661a..73629833 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -11,7 +11,6 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.LithoBottomSheet @@ -28,78 +27,82 @@ class OpenSourceBottomSheet : LithoBottomSheet() { val creatorName = componentContext.getString(R.string.maubis_apps) val openSourceDetails = getString(R.string.about_page_description_os, appName, creatorName) val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.osp_page_about_osp) - .marginDip(YogaEdge.HORIZONTAL, 0f)) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 16f) - .text(openSourceDetails) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_xlarge) - .marginDip(YogaEdge.BOTTOM, 4f) - .textRes(R.string.osp_page_libraries) - .typeface(CoreConfig.FONT_MONSERRAT) - .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) - .child(Text.create(componentContext) - .textSizeRes(R.dimen.font_size_large) - .marginDip(YogaEdge.BOTTOM, 4f) - .text(Markdown.render(LIBRARY_DETAILS_MD, true)) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.about_page_contribute) - .onPrimaryClick { - try { - activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_URL))) - dismiss() - } catch (exception: Exception) { - maybeThrow(activity, exception) - } - } - .paddingDip(YogaEdge.VERTICAL, 8f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.osp_page_about_osp) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 16f) + .text(openSourceDetails) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_xlarge) + .marginDip(YogaEdge.BOTTOM, 4f) + .textRes(R.string.osp_page_libraries) + .typeface(CoreConfig.FONT_MONSERRAT) + .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) + .child( + Text.create(componentContext) + .textSizeRes(R.dimen.font_size_large) + .marginDip(YogaEdge.BOTTOM, 4f) + .text(Markdown.render(LIBRARY_DETAILS_MD, true)) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.about_page_contribute) + .onPrimaryClick { + try { + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_URL))) + dismiss() + } catch (exception: Exception) { + maybeThrow(activity, exception) + } + } + .paddingDip(YogaEdge.VERTICAL, 8f)) return component.build() } companion object { val GITHUB_URL = "https://github.com/BijoySingh/Material-Notes-Android-App" val LIBRARY_DETAILS_MD = "**Android Support Libraries**\n" + - "- `'com.android.support.appcompat-v7'`\n" + - "- `'com.android.support.recyclerview-v7'`\n" + - "- `'com.android.support.cardview-v7'`\n" + - "- `'com.android.support.support-v4'`\n" + - "- `'com.android.support.design'`\n" + - "- `'com.android.support.constraint'`\n\n" + - "**Android Architecture Room Library**\n" + - "- `'android.arch.persistence.room'`\n\n" + - "**Internal Support Libraries**\n" + - "- `'com.github.bijoysingh.android-basics'`\n" + - "- `'com.github.bijoysingh.ui-basics'`\n" + - "- `'com.github.bijoysingh.floating-bubble'`\n\n" + - "**Kotlin Support**\n" + - "- `'org.jetbrains.kotlin'`\n" + - "- `'org.jetbrains.kotlinx'`\n\n" + - "**Reprint: Fingerprint Library**\n" + - "- `'com.github.ajalt.reprint'`\n\n" + - "**Google Firebase Support Library**\n" + - "- `'com.google.firebase:firebase-auth'`\n" + - "- `'com.google.firebase:firebase-database'`\n\n" + - "**Google Play Services Library**\n" + - "- `'com.google.android.gms:play-services-auth'`\n\n" + - "**Shortcuts Gradle Plugin**\n" + - "- `'com.github.zellius:android-shortcut-gradle-plugin'`\n\n" + - "**Facebook Litho and SoLoader**\n" + - "- `'com.facebook.litho'`\n" + - "- `'com.facebook.soloader:soloader'`\n\n" + - "**Easy Image**\n" + - "- `'com.github.jkwiecien:EasyImage'`\n\n" + - "**Evernote Android Job**\n" + - "- `'com.evernote:android-job'`\n\n" + - "**Google Flexbox Library**\n" + - "- `'com.google.android:flexbox'`\n" + "- `'com.android.support.appcompat-v7'`\n" + + "- `'com.android.support.recyclerview-v7'`\n" + + "- `'com.android.support.cardview-v7'`\n" + + "- `'com.android.support.support-v4'`\n" + + "- `'com.android.support.design'`\n" + + "- `'com.android.support.constraint'`\n\n" + + "**Android Architecture Room Library**\n" + + "- `'android.arch.persistence.room'`\n\n" + + "**Internal Support Libraries**\n" + + "- `'com.github.bijoysingh.android-basics'`\n" + + "- `'com.github.bijoysingh.ui-basics'`\n" + + "- `'com.github.bijoysingh.floating-bubble'`\n\n" + + "**Kotlin Support**\n" + + "- `'org.jetbrains.kotlin'`\n" + + "- `'org.jetbrains.kotlinx'`\n\n" + + "**Reprint: Fingerprint Library**\n" + + "- `'com.github.ajalt.reprint'`\n\n" + + "**Google Firebase Support Library**\n" + + "- `'com.google.firebase:firebase-auth'`\n" + + "- `'com.google.firebase:firebase-database'`\n\n" + + "**Google Play Services Library**\n" + + "- `'com.google.android.gms:play-services-auth'`\n\n" + + "**Shortcuts Gradle Plugin**\n" + + "- `'com.github.zellius:android-shortcut-gradle-plugin'`\n\n" + + "**Facebook Litho and SoLoader**\n" + + "- `'com.facebook.litho'`\n" + + "- `'com.facebook.soloader:soloader'`\n\n" + + "**Easy Image**\n" + + "- `'com.github.jkwiecien:EasyImage'`\n\n" + + "**Evernote Android Job**\n" + + "- `'com.evernote:android-job'`\n\n" + + "**Google Flexbox Library**\n" + + "- `'com.google.android:flexbox'`\n" } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt index 6446c8ed..6c8a9466 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SecurityOptionsBottomSheet.kt @@ -39,7 +39,8 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = context as ThemedActivity val options = ArrayList() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.security_option_set_pin_code, subtitle = R.string.security_option_set_pin_code_subtitle, icon = R.drawable.ic_option_security, @@ -51,10 +52,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = isPinCodeEnabled() - )) + )) val isLite = FlavorUtils.isLite() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.security_option_lock_app, subtitle = R.string.security_option_lock_app_details, icon = R.drawable.ic_apps_white_48dp, @@ -66,11 +68,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { when { isPinCodeEnabled() -> openVerifySheet( - activity = activity, - onVerifySuccess = { - sSecurityAppLockEnabled = !sSecurityAppLockEnabled - reset(componentContext.androidContext, dialog) - } + activity = activity, + onVerifySuccess = { + sSecurityAppLockEnabled = !sSecurityAppLockEnabled + reset(componentContext.androidContext, dialog) + } ) else -> openCreatePasswordDialog(dialog) } @@ -80,9 +82,10 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { isLite -> R.drawable.ic_rating else -> 0 } - )) + )) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.security_option_ask_pin_always, subtitle = R.string.security_option_ask_pin_always_details, icon = R.drawable.ic_action_grid, @@ -94,11 +97,11 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { when { isPinCodeEnabled() -> openVerifySheet( - activity = activity, - onVerifySuccess = { - sSecurityAskPinAlways = !sSecurityAskPinAlways - reset(componentContext.androidContext, dialog) - } + activity = activity, + onVerifySuccess = { + sSecurityAskPinAlways = !sSecurityAskPinAlways + reset(componentContext.androidContext, dialog) + } ) else -> openCreatePasswordDialog(dialog) } @@ -110,21 +113,22 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { sSecurityAskPinAlways -> R.drawable.ic_done_white_48dp else -> 0 } - )) + )) val hasFingerprint = Reprint.hasFingerprintRegistered() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.security_option_fingerprint_enabled, subtitle = R.string.security_option_fingerprint_enabled_subtitle, icon = R.drawable.ic_option_fingerprint, listener = { when { isPinCodeEnabled() -> openVerifySheet( - activity = activity, - onVerifySuccess = { - sSecurityFingerprintEnabled = false - reset(componentContext.androidContext, dialog) - } + activity = activity, + onVerifySuccess = { + sSecurityFingerprintEnabled = false + reset(componentContext.androidContext, dialog) + } ) else -> { sSecurityFingerprintEnabled = false @@ -135,19 +139,20 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { visible = sSecurityFingerprintEnabled && hasFingerprint, isSelectable = true, selected = true - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.security_option_fingerprint_disabled, subtitle = R.string.security_option_fingerprint_disabled_subtitle, icon = R.drawable.ic_option_fingerprint, listener = { when { isPinCodeEnabled() -> openVerifySheet( - activity = activity, - onVerifySuccess = { - sSecurityFingerprintEnabled = true - reset(componentContext.androidContext, dialog) - } + activity = activity, + onVerifySuccess = { + sSecurityFingerprintEnabled = true + reset(componentContext.androidContext, dialog) + } ) else -> { sSecurityFingerprintEnabled = true @@ -156,26 +161,26 @@ class SecurityOptionsBottomSheet : LithoOptionBottomSheet() { } }, visible = !sSecurityFingerprintEnabled && hasFingerprint - )) + )) return options } fun openCreatePasswordDialog(dialog: Dialog) { val activity = context as ThemedActivity openCreateSheet( - activity = activity, - onCreateSuccess = { reset(dialog.context, dialog) }) + activity = activity, + onCreateSuccess = { reset(dialog.context, dialog) }) } fun openResetPasswordDialog(dialog: Dialog) { val activity = context as ThemedActivity openVerifySheet( - activity, - onVerifySuccess = { - openCreatePasswordDialog(dialog) - }, - onVerifyFailure = { - openResetPasswordDialog(dialog) - }) + activity, + onVerifySuccess = { + openCreatePasswordDialog(dialog) + }, + onVerifyFailure = { + openResetPasswordDialog(dialog) + }) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt index c6d6c6fc..48e805a6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SettingsOptionsBottomSheet.kt @@ -27,7 +27,8 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { val isLoggedIn = ApplicationBase.instance.authenticator().isLoggedIn(activity) val migrateToPro = getMigrateToProAppInformationItem(activity) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = migrateToPro.title, subtitle = migrateToPro.source, icon = migrateToPro.icon, @@ -37,8 +38,9 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, visible = FlavorUtils.isLite() && FlavorUtils.hasProAppInstalled(activity), selected = true - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.home_option_login_with_app, subtitle = R.string.home_option_login_with_app_subtitle, icon = R.drawable.ic_sign_in_options, @@ -47,57 +49,58 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { dismiss() }, visible = loginClick !== null && !isLoggedIn - )) + )) options.add(LithoOptionsItem( - title = R.string.home_option_ui_experience, - subtitle = R.string.home_option_ui_experience_subtitle, - icon = R.drawable.ic_action_grid, - listener = { - UISettingsOptionsBottomSheet.openSheet(activity) - } + title = R.string.home_option_ui_experience, + subtitle = R.string.home_option_ui_experience_subtitle, + icon = R.drawable.ic_action_grid, + listener = { + UISettingsOptionsBottomSheet.openSheet(activity) + } )) options.add(LithoOptionsItem( - title = R.string.home_option_editor_options_title, - subtitle = R.string.home_option_editor_options_description, - icon = R.drawable.ic_edit_white_48dp, - listener = { - openSheet(activity, EditorOptionsBottomSheet()) - } + title = R.string.home_option_editor_options_title, + subtitle = R.string.home_option_editor_options_description, + icon = R.drawable.ic_edit_white_48dp, + listener = { + openSheet(activity, EditorOptionsBottomSheet()) + } )) options.add(LithoOptionsItem( - title = R.string.home_option_backup_options, - subtitle = R.string.home_option_backup_options_subtitle, - icon = R.drawable.ic_export, - listener = { - openSheet(activity, BackupSettingsOptionsBottomSheet()) - } + title = R.string.home_option_backup_options, + subtitle = R.string.home_option_backup_options_subtitle, + icon = R.drawable.ic_export, + listener = { + openSheet(activity, BackupSettingsOptionsBottomSheet()) + } )) options.add(LithoOptionsItem( - title = R.string.home_option_security, - subtitle = R.string.home_option_security_subtitle, - icon = R.drawable.ic_option_security, - listener = { - openSheet(activity, SecurityOptionsBottomSheet()) - dismiss() - } + title = R.string.home_option_security, + subtitle = R.string.home_option_security_subtitle, + icon = R.drawable.ic_option_security, + listener = { + openSheet(activity, SecurityOptionsBottomSheet()) + dismiss() + } )) options.add(LithoOptionsItem( - title = R.string.home_option_widget_options_title, - subtitle = R.string.home_option_widget_options_description, - icon = R.drawable.icon_widget, - listener = { - openSheet(activity, WidgetOptionsBottomSheet()) - } + title = R.string.home_option_widget_options_title, + subtitle = R.string.home_option_widget_options_description, + icon = R.drawable.icon_widget, + listener = { + openSheet(activity, WidgetOptionsBottomSheet()) + } )) options.add(LithoOptionsItem( - title = R.string.home_option_about, - subtitle = R.string.home_option_about_subtitle, - icon = R.drawable.ic_info, - listener = { - openSheet(activity, AboutSettingsOptionsBottomSheet()) - } + title = R.string.home_option_about, + subtitle = R.string.home_option_about_subtitle, + icon = R.drawable.ic_info, + listener = { + openSheet(activity, AboutSettingsOptionsBottomSheet()) + } )) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.home_option_install_pro_app, subtitle = R.string.home_option_install_pro_app_details, icon = R.drawable.ic_favorite_white_48dp, @@ -106,25 +109,26 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { dismiss() }, visible = FlavorUtils.isLite() && !FlavorUtils.hasProAppInstalled(activity) - )) + )) options.add(LithoOptionsItem( - title = R.string.home_option_rate_and_review, - subtitle = R.string.home_option_rate_and_review_subtitle, - icon = R.drawable.ic_rating, - listener = { - IntentUtils.openAppPlayStore(activity) - dismiss() - } + title = R.string.home_option_rate_and_review, + subtitle = R.string.home_option_rate_and_review_subtitle, + icon = R.drawable.ic_rating, + listener = { + IntentUtils.openAppPlayStore(activity) + dismiss() + } )) options.add(LithoOptionsItem( - title = R.string.home_option_delete_notes_and_more, - subtitle = R.string.home_option_delete_notes_and_more_details, - icon = R.drawable.ic_delete_permanently, - listener = { - openSheet(activity, DeleteAndMoreOptionsBottomSheet()) - } + title = R.string.home_option_delete_notes_and_more, + subtitle = R.string.home_option_delete_notes_and_more_details, + icon = R.drawable.ic_delete_permanently, + listener = { + openSheet(activity, DeleteAndMoreOptionsBottomSheet()) + } )) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.home_option_logout_of_app, subtitle = R.string.home_option_logout_of_app_subtitle, icon = R.drawable.ic_sign_in_options, @@ -139,7 +143,7 @@ class SettingsOptionsBottomSheet : LithoOptionBottomSheet() { dismiss() }, visible = isLoggedIn - )) + )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt index 77069bf0..259f2c50 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/SortingOptionsBottomSheet.kt @@ -19,7 +19,8 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { val options = ArrayList() SortingTechnique.values().forEach { technique -> - options.add(LithoChooseOptionsItem( + options.add( + LithoChooseOptionsItem( title = getSortingTechniqueLabel(technique), listener = { setSortingState(technique) @@ -27,7 +28,7 @@ class SortingOptionsBottomSheet : LithoChooseOptionBottomSheet() { reset(componentContext.androidContext, dialog) }, selected = sorting == technique - )) + )) } return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt index 80f2f8d6..b5260ab5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/ThemeColorPickerBottomSheet.kt @@ -3,7 +3,11 @@ package com.maubis.scarlet.base.settings.sheet import android.app.Dialog import android.graphics.Color import android.support.v7.app.AppCompatActivity -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -11,10 +15,13 @@ import com.facebook.litho.annotations.Prop import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet -import com.maubis.scarlet.base.support.sheets.* +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoOptionsItem +import com.maubis.scarlet.base.support.sheets.OptionItemLayout +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.sheets.openSheet import com.maubis.scarlet.base.support.specs.BottomSheetBar import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.specs.RoundIcon @@ -29,40 +36,42 @@ import com.maubis.scarlet.base.support.utils.OsVersionUtils @LayoutSpec object ThemeColorPickerItemSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop theme: Theme, - @Prop isDisabled: Boolean, - @Prop isSelected: Boolean): Component { + fun onCreate( + context: ComponentContext, + @Prop theme: Theme, + @Prop isDisabled: Boolean, + @Prop isSelected: Boolean): Component { val icon = RoundIcon.create(context) - .showBorder(true) - .iconSizeDip(64f) - .iconPaddingDip(16f) - .onClick { } - .flexGrow(1f) - .isClickDisabled(true) - .alpha(if (isDisabled) 0.3f else 1f) + .showBorder(true) + .iconSizeDip(64f) + .iconPaddingDip(16f) + .onClick { } + .flexGrow(1f) + .isClickDisabled(true) + .alpha(if (isDisabled) 0.3f else 1f) when (isSelected) { true -> icon.iconRes(R.drawable.ic_done_white_48dp) - .bgColorRes(R.color.colorAccent) - .iconColor(Color.WHITE) + .bgColorRes(R.color.colorAccent) + .iconColor(Color.WHITE) false -> icon.iconRes(R.drawable.icon_realtime_markdown) - .bgColorRes(theme.background) - .iconColorRes(theme.primaryText) + .bgColorRes(theme.background) + .iconColorRes(theme.primaryText) } val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .child(icon) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .child(icon) row.clickHandler(ThemeColorPickerItem.onItemClick(context)) return row.build() } @OnEvent(ClickEvent::class) - fun onItemClick(context: ComponentContext, - @Prop theme: Theme, - @Prop isDisabled: Boolean, - @Prop onThemeSelected: (Theme) -> Unit) { + fun onItemClick( + context: ComponentContext, + @Prop theme: Theme, + @Prop isDisabled: Boolean, + @Prop onThemeSelected: (Theme) -> Unit) { if (isDisabled) { openSheet(context.androidContext as ThemedActivity, InstallProUpsellBottomSheet()) return @@ -77,38 +86,40 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val column = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.theme_page_title) - .marginDip(YogaEdge.HORIZONTAL, 0f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.theme_page_title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) if (OsVersionUtils.canUseSystemTheme()) { column.child(OptionItemLayout.create(componentContext) - .option(LithoOptionsItem( - title = R.string.theme_use_system_theme, - subtitle = R.string.theme_use_system_theme_details, - icon = R.drawable.ic_action_color, - listener = {}, - isSelectable = true, - selected = sAutomaticTheme, - actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 - )) - .onClick { - val context = componentContext.androidContext as AppCompatActivity - if (FlavorUtils.isLite()) { - openSheet(context, InstallProUpsellBottomSheet()) - return@onClick - } + .option( + LithoOptionsItem( + title = R.string.theme_use_system_theme, + subtitle = R.string.theme_use_system_theme_details, + icon = R.drawable.ic_action_color, + listener = {}, + isSelectable = true, + selected = sAutomaticTheme, + actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 + )) + .onClick { + val context = componentContext.androidContext as AppCompatActivity + if (FlavorUtils.isLite()) { + openSheet(context, InstallProUpsellBottomSheet()) + return@onClick + } - sAutomaticTheme = !sAutomaticTheme - if (sAutomaticTheme) { - setThemeFromSystem(context) - onThemeChange(sAppTheme.get()) - } - reset(context, dialog) - }) + sAutomaticTheme = !sAutomaticTheme + if (sAutomaticTheme) { + setThemeFromSystem(context) + onThemeChange(sAppTheme.get()) + } + reset(context, dialog) + }) } if (!sAutomaticTheme) { @@ -117,9 +128,9 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { if (index % 4 == 0) { column.child(flex) flex = Row.create(componentContext) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.VERTICAL, 12f) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) } val disabled = when { @@ -128,24 +139,24 @@ class ThemeColorPickerBottomSheet : LithoBottomSheet() { else -> true } flex?.child( - ThemeColorPickerItem.create(componentContext) - .theme(theme) - .isDisabled(disabled) - .isSelected(theme.name == getThemeFromStore().name) - .onThemeSelected { newTheme -> - onThemeChange(newTheme) - } - .flexGrow(1f)) + ThemeColorPickerItem.create(componentContext) + .theme(theme) + .isDisabled(disabled) + .isSelected(theme.name == getThemeFromStore().name) + .onThemeSelected { newTheme -> + onThemeChange(newTheme) + } + .flexGrow(1f)) } column.child(flex) } column.child(EmptySpec.create(componentContext).widthPercent(100f).heightDip(24f)) column.child(BottomSheetBar.create(componentContext) - .primaryActionRes(R.string.import_export_layout_exporting_done) - .onPrimaryClick { - dismiss() - }.paddingDip(YogaEdge.VERTICAL, 8f)) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) return column.build() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index 6d273771..70536eb4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -5,7 +5,6 @@ import com.facebook.litho.ComponentContext import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet @@ -24,15 +23,16 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { val activity = componentContext.androidContext as MainActivity val options = ArrayList() options.add(LithoOptionsItem( - title = R.string.home_option_theme_color, - subtitle = R.string.home_option_theme_color_subtitle, - icon = if (sAppTheme.isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, - listener = { - activity.performAction(MainActivityActions.COLOR_PICKER) - } + title = R.string.home_option_theme_color, + subtitle = R.string.home_option_theme_color_subtitle, + icon = if (sAppTheme.isNightTheme()) R.drawable.night_mode_white_48dp else R.drawable.ic_action_day_mode, + listener = { + activity.performAction(MainActivityActions.COLOR_PICKER) + } )) val isTablet = resources.getBoolean(R.bool.is_tablet) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.home_option_enable_list_view, subtitle = R.string.home_option_enable_list_view_subtitle, icon = R.drawable.ic_action_list, @@ -42,8 +42,9 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { reset(activity, dialog) }, visible = !isTablet && useGridView - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.home_option_enable_grid_view, subtitle = R.string.home_option_enable_grid_view_subtitle, icon = R.drawable.ic_action_grid, @@ -53,17 +54,18 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { reset(activity, dialog) }, visible = !isTablet && !useGridView - )) + )) options.add(LithoOptionsItem( - title = R.string.home_option_order_notes, - subtitle = getSortingTechniqueLabel(getSortingState()), - icon = R.drawable.ic_sort, - listener = { - SortingOptionsBottomSheet.openSheet(activity, { activity.setupData() }) - reset(activity, dialog) - } + title = R.string.home_option_order_notes, + subtitle = getSortingTechniqueLabel(getSortingState()), + icon = R.drawable.ic_sort, + listener = { + SortingOptionsBottomSheet.openSheet(activity, { activity.setupData() }) + reset(activity, dialog) + } )) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.note_option_font_size, subtitle = 0, content = activity.getString(R.string.note_option_font_size_subtitle, sEditorTextSize), @@ -76,17 +78,18 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { reset(activity, dialog) }, actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 - )) + )) options.add(LithoOptionsItem( - title = R.string.note_option_number_lines, - subtitle = 0, - content = activity.getString(R.string.note_option_number_lines_subtitle, sNoteItemLineCount), - icon = R.drawable.ic_action_list, - listener = { - openSheet(activity, LineCountBottomSheet()) - } + title = R.string.note_option_number_lines, + subtitle = 0, + content = activity.getString(R.string.note_option_number_lines_subtitle, sNoteItemLineCount), + icon = R.drawable.ic_action_list, + listener = { + openSheet(activity, LineCountBottomSheet()) + } )) - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.ui_options_note_background_color, subtitle = when (useNoteColorAsBackground) { true -> R.string.ui_options_note_background_color_settings_note @@ -103,8 +106,9 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { reset(activity, dialog) }, actionIcon = if (FlavorUtils.isLite()) R.drawable.ic_rating else 0 - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.markdown_sheet_home_markdown_support, subtitle = R.string.markdown_sheet_home_markdown_support_subtitle, icon = R.drawable.ic_markdown_logo, @@ -114,7 +118,7 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sMarkdownEnabledHome - )) + )) return options } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/view/ColorView.kt b/base/src/main/java/com/maubis/scarlet/base/settings/view/ColorView.kt index 4dc00953..73940a01 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/view/ColorView.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/view/ColorView.kt @@ -22,7 +22,6 @@ class ColorView : LinearLayout { init(context) } - constructor(context: Context, layout: Int) : super(context) { init(context, layout) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt b/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt index ceb10dc2..3951c0b3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/BitmapHelper.kt @@ -10,9 +10,9 @@ import java.io.File import java.io.FileOutputStream object BitmapHelper { - fun send(context: Context, bitmap: Bitmap) { + fun send(context: Context, bitmap: Bitmap) { val path = MediaStore.Images.Media.insertImage( - context.contentResolver, bitmap, "Scarlet Image", "Scarlet Image") + context.contentResolver, bitmap, "Scarlet Image", "Scarlet Image") val uri = Uri.parse(path) val intent = Intent(Intent.ACTION_SEND) @@ -21,15 +21,15 @@ object BitmapHelper { context.startActivity(Intent.createChooser(intent, "Share Image")) } - fun send(context: Context, bitmaps: List) { + fun send(context: Context, bitmaps: List) { val fileUris = ArrayList() bitmaps - .mapIndexed { index, bitmap -> - MediaStore.Images.Media.insertImage( - context.contentResolver, bitmap, "Scarlet Image ($index)", "Scarlet Image ($index)") - }.map { - Uri.parse(it) - }.forEach { fileUris.add(it) } + .mapIndexed { index, bitmap -> + MediaStore.Images.Media.insertImage( + context.contentResolver, bitmap, "Scarlet Image ($index)", "Scarlet Image ($index)") + }.map { + Uri.parse(it) + }.forEach { fileUris.add(it) } val intent = Intent(Intent.ACTION_SEND_MULTIPLE) intent.type = "image/jpeg" intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris) @@ -45,7 +45,7 @@ object BitmapHelper { return null } - fun saveToFile(cacheFile: File, bitmap: Bitmap) { + fun saveToFile(cacheFile: File, bitmap: Bitmap) { val fOut = FileOutputStream(cacheFile) bitmap.compress(Bitmap.CompressFormat.PNG, 90, fOut) fOut.flush() diff --git a/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt b/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt index cf5412b2..62f39932 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/SearchConfig.kt @@ -14,20 +14,20 @@ import com.maubis.scarlet.base.note.isNoteLockedButAppUnlocked import com.maubis.scarlet.base.settings.sheet.SortingOptionsBottomSheet class SearchConfig( - var text: String = "", - var mode: HomeNavigationState = HomeNavigationState.DEFAULT, - var colors: MutableList = emptyList().toMutableList(), - var tags: MutableList = emptyList().toMutableList(), - var folders: MutableList = emptyList().toMutableList()) { + var text: String = "", + var mode: HomeNavigationState = HomeNavigationState.DEFAULT, + var colors: MutableList = emptyList().toMutableList(), + var tags: MutableList = emptyList().toMutableList(), + var folders: MutableList = emptyList().toMutableList()) { fun hasFolder(folder: Folder) = folders.firstOrNull { it.uuid == folder.uuid } !== null fun hasFilter(): Boolean { return folders.isNotEmpty() - || tags.isNotEmpty() - || colors.isNotEmpty() - || text.isNotBlank() - || mode !== HomeNavigationState.DEFAULT; + || tags.isNotEmpty() + || colors.isNotEmpty() + || text.isNotBlank() + || mode !== HomeNavigationState.DEFAULT; } fun clear(): SearchConfig { @@ -53,23 +53,23 @@ class SearchConfig( fun copy(): SearchConfig { return SearchConfig( - text, - mode, - colors.filter { true }.toMutableList(), - tags.filter { true }.toMutableList(), - folders.filter { true }.toMutableList()) + text, + mode, + colors.filter { true }.toMutableList(), + tags.filter { true }.toMutableList(), + folders.filter { true }.toMutableList()) } } fun unifiedSearchSynchronous(config: SearchConfig): List { val sorting = SortingOptionsBottomSheet.getSortingState() val notes = unifiedSearchWithoutFolder(config) - .filter { - when (config.folders.isEmpty()) { - true -> it.folder.isBlank() - false -> config.folders.map { it.uuid }.contains(it.folder) - } + .filter { + when (config.folders.isEmpty()) { + true -> it.folder.isBlank() + false -> config.folders.map { it.uuid }.contains(it.folder) } + } return sort(notes, sorting) } @@ -88,15 +88,15 @@ fun filterOutFolders(notes: List): List { fun unifiedSearchWithoutFolder(config: SearchConfig): List { return getNotesForMode(config) - .filter { config.colors.isEmpty() || config.colors.contains(it.color) } - .filter { note -> config.tags.isEmpty() || config.tags.filter { note.tags !== null && note.tags.contains(it.uuid) }.isNotEmpty() } - .filter { - when { - config.text.isBlank() -> true - it.locked && !it.isNoteLockedButAppUnlocked() -> false - else -> it.getFullText().contains(config.text, true) - } + .filter { config.colors.isEmpty() || config.colors.contains(it.color) } + .filter { note -> config.tags.isEmpty() || config.tags.filter { note.tags !== null && note.tags.contains(it.uuid) }.isNotEmpty() } + .filter { + when { + config.text.isBlank() -> true + it.locked && !it.isNoteLockedButAppUnlocked() -> false + else -> it.getFullText().contains(config.text, true) } + } } fun filterDirectlyValidFolders(config: SearchConfig): List { @@ -105,8 +105,8 @@ fun filterDirectlyValidFolders(config: SearchConfig): List { } return foldersDb.getAll() - .filter { config.colors.isEmpty() || config.colors.contains(it.color) } - .filter { it.title.contains(config.text, true) } + .filter { config.colors.isEmpty() || config.colors.contains(it.color) } + .filter { it.title.contains(config.text, true) } } fun getNotesForMode(config: SearchConfig): List { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt index 9346d4dc..f2e4291d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ShortcutUtils.kt @@ -21,8 +21,9 @@ fun addShortcut(context: Context, shortcut: ShortcutInfo) { val pinShortcutInfo = ShortcutInfo.Builder(context, shortcut.id).build() val pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo) - val successCallback = PendingIntent.getBroadcast(context, 0, - pinnedShortcutCallbackIntent, 0) + val successCallback = PendingIntent.getBroadcast( + context, 0, + pinnedShortcutCallbackIntent, 0) shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt index bbc80274..00e5deb4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/HouseKeeper.kt @@ -19,11 +19,11 @@ import java.util.concurrent.TimeUnit class HouseKeeper(val context: Context) { private val houseKeeperTasks: Array<() -> Unit> = arrayOf( - { removeOlderClips() }, - { removeDecoupledFolders() }, - { removeOldReminders() }, - { deleteRedundantImageFiles() }, - { migrateZeroUidNotes() } + { removeOlderClips() }, + { removeDecoupledFolders() }, + { removeOldReminders() }, + { deleteRedundantImageFiles() }, + { migrateZeroUidNotes() } ) fun execute() { @@ -34,7 +34,7 @@ class HouseKeeper(val context: Context) { fun removeOlderClips(deltaTimeMs: Long = 604800000L) { val notes = notesDb.database() - .getOldTrashedNotes(Calendar.getInstance().timeInMillis - deltaTimeMs) + .getOldTrashedNotes(Calendar.getInstance().timeInMillis - deltaTimeMs) for (note in notes) { note.delete(context) } @@ -43,13 +43,13 @@ class HouseKeeper(val context: Context) { private fun removeDecoupledFolders() { val folders = foldersDb.getAll().map { it.uuid } notesDb.getAll() - .filter { it.folder.isNotBlank() } - .forEach { - if (!folders.contains(it.folder)) { - it.folder = "" - it.save(context) - } + .filter { it.folder.isNotBlank() } + .forEach { + if (!folders.contains(it.folder)) { + it.folder = "" + it.save(context) } + } } private fun removeOldReminders() { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt index 842a5b2e..3aa1b27f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/database/MigrationUtils.kt @@ -63,8 +63,8 @@ class Migrator(val context: Context) { File(context.cacheDir, "images").renameTo(File(context.filesDir, "images")) } runTaskIf( - getLastUsedAppVersionCode() == 0, - KEY_MIGRATE_DEFAULT_VALUES) { + getLastUsedAppVersionCode() == 0, + KEY_MIGRATE_DEFAULT_VALUES) { sAppThemeLabel = Theme.DARK.name sAppPreferences.put(KEY_LIST_VIEW, true) } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/option/OptionsItem.kt b/base/src/main/java/com/maubis/scarlet/base/support/option/OptionsItem.kt index cb90dc85..97d04699 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/option/OptionsItem.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/option/OptionsItem.kt @@ -3,13 +3,13 @@ package com.maubis.scarlet.base.support.option import android.view.View class OptionsItem( - val title: Int, - val subtitle: Int, - val icon: Int, - val selected: Boolean = false, // indicates its a selected option (blue color) - val visible: Boolean = true, // indicates if the option is visible - val enabled: Boolean = false, // indicates if the option will show a checked on the side - val invalid: Boolean = false, // indicates that the option will be faded and click removed - val content: String = "", // content is an alternative to subtitle when it's 0 - val actionIcon: Int = 0, // icon resource for the action - val listener: View.OnClickListener) \ No newline at end of file + val title: Int, + val subtitle: Int, + val icon: Int, + val selected: Boolean = false, // indicates its a selected option (blue color) + val visible: Boolean = true, // indicates if the option is visible + val enabled: Boolean = false, // indicates if the option will show a checked on the side + val invalid: Boolean = false, // indicates that the option will be faded and click removed + val content: String = "", // content is an alternative to subtitle when it's 0 + val actionIcon: Int = 0, // icon resource for the action + val listener: View.OnClickListener) \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/option/TagOptionsItemBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/option/TagOptionsItemBase.kt index 10e0cc34..fc51649a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/option/TagOptionsItemBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/option/TagOptionsItemBase.kt @@ -4,12 +4,12 @@ import android.view.View import com.maubis.scarlet.base.database.room.tag.Tag abstract class TagOptionsItemBase( - val tag: Tag, - val usages: Int, - val selected: Boolean, - val editable: Boolean, - val editListener: View.OnClickListener? = null, - val listener: View.OnClickListener) : Comparable { + val tag: Tag, + val usages: Int, + val selected: Boolean, + val editable: Boolean, + val editListener: View.OnClickListener? = null, + val listener: View.OnClickListener) : Comparable { abstract fun getIcon(): Int diff --git a/base/src/main/java/com/maubis/scarlet/base/support/recycler/SimpleItemTouchHelper.kt b/base/src/main/java/com/maubis/scarlet/base/support/recycler/SimpleItemTouchHelper.kt index eea5a418..2a0424a0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/recycler/SimpleItemTouchHelper.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/recycler/SimpleItemTouchHelper.kt @@ -9,16 +9,18 @@ class SimpleItemTouchHelper(private val mAdapter: ItemTouchHelperAdapter) : Item override fun isItemViewSwipeEnabled(): Boolean = true - override fun getMovementFlags(recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder): Int { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder): Int { val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN val swipeFlags = 0 //ItemTouchHelper.START | ItemTouchHelper.END; return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) } - override fun onMove(recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder): Boolean { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder): Boolean { mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition) return true } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt index 7567cc28..08c31d64 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt @@ -6,7 +6,6 @@ import android.widget.GridLayout import android.widget.TextView import com.github.bijoysingh.uibasics.views.UILabelView import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt index fe097964..10e0e5db 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridOptionBottomSheet.kt @@ -9,7 +9,6 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.support.specs.GridSectionItem import com.maubis.scarlet.base.support.specs.GridSectionView - abstract class GridOptionBottomSheet : LithoBottomSheet() { abstract fun title(): Int @@ -17,21 +16,21 @@ abstract class GridOptionBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val column = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child(getLithoBottomSheetTitle(componentContext).textRes(title())) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child(getLithoBottomSheetTitle(componentContext).textRes(title())) val options = getOptions(componentContext, dialog) var index = 0 options.forEach { index++ column.child( - GridSectionView.create(componentContext) - .marginDip(YogaEdge.HORIZONTAL, 12f) - .marginDip(YogaEdge.VERTICAL, 8f) - .iconSizeRes(R.dimen.primary_round_icon_size) - .showSeparator(index != options.size) - .section(it)) + GridSectionView.create(componentContext) + .marginDip(YogaEdge.HORIZONTAL, 12f) + .marginDip(YogaEdge.VERTICAL, 8f) + .iconSizeRes(R.dimen.primary_round_icon_size) + .showSeparator(index != options.size) + .section(it)) } return column.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index 05cf6cdf..e6195c90 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -18,7 +18,6 @@ import com.facebook.litho.widget.VerticalScroll import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT @@ -31,23 +30,23 @@ fun openSheet(activity: AppCompatActivity, sheet: LithoBottomSheet) { fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { return Text.create(context) - .textSizeRes(R.dimen.font_size_xxxlarge) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD) - .marginDip(YogaEdge.HORIZONTAL, 20f) - .marginDip(YogaEdge.TOP, 18f) - .marginDip(YogaEdge.BOTTOM, 8f) - .textStyle(Typeface.BOLD) - .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .textSizeRes(R.dimen.font_size_xxxlarge) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD) + .marginDip(YogaEdge.HORIZONTAL, 20f) + .marginDip(YogaEdge.TOP, 18f) + .marginDip(YogaEdge.BOTTOM, 8f) + .textStyle(Typeface.BOLD) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) } fun getLithoBottomSheetButton(context: ComponentContext): Text.Builder { return Text.create(context) - .typeface(FONT_MONSERRAT) - .textSizeRes(R.dimen.font_size_large) - .paddingDip(YogaEdge.VERTICAL, 12f) - .paddingDip(YogaEdge.HORIZONTAL, 24f) - .textColorRes(R.color.light_secondary_text) - .backgroundRes(R.drawable.accent_rounded_bg) + .typeface(FONT_MONSERRAT) + .textSizeRes(R.dimen.font_size_large) + .paddingDip(YogaEdge.VERTICAL, 12f) + .paddingDip(YogaEdge.HORIZONTAL, 24f) + .textColorRes(R.color.light_secondary_text) + .backgroundRes(R.drawable.accent_rounded_bg) } abstract class LithoBottomSheet : BottomSheetDialogFragment() { @@ -89,24 +88,25 @@ abstract class LithoBottomSheet : BottomSheetDialogFragment() { } val baseComponent = Column.create(componentContext) - .paddingDip(YogaEdge.TOP, topMargin()) - .paddingDip(YogaEdge.BOTTOM, bottomMargin()) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .backgroundColor(backgroundColor(componentContext)) - .child( - Image.create(componentContext) - .drawableRes(topHandle) - .widthDip(72f) - .heightDip(6f) - .alpha(0.8f) - .marginDip(YogaEdge.BOTTOM, 8f) - .build() - ) - .child(VerticalScroll.create(componentContext) - .nestedScrollingEnabled(true) - .childComponent(childComponent)) - .build() + .paddingDip(YogaEdge.TOP, topMargin()) + .paddingDip(YogaEdge.BOTTOM, bottomMargin()) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .backgroundColor(backgroundColor(componentContext)) + .child( + Image.create(componentContext) + .drawableRes(topHandle) + .widthDip(72f) + .heightDip(6f) + .alpha(0.8f) + .marginDip(YogaEdge.BOTTOM, 8f) + .build() + ) + .child( + VerticalScroll.create(componentContext) + .nestedScrollingEnabled(true) + .childComponent(childComponent)) + .build() val contentView = LithoView.create(componentContext.androidContext, baseComponent) dialog.setContentView(contentView) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt index 78276b1c..93e129af 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt @@ -3,7 +3,11 @@ package com.maubis.scarlet.base.support.sheets import android.app.Dialog import android.graphics.Color import android.graphics.Typeface -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -12,48 +16,49 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType class LithoChooseOptionsItem( - val title: Int, - val selected: Boolean = false, - val listener: () -> Unit) + val title: Int, + val selected: Boolean = false, + val listener: () -> Unit) @LayoutSpec object ChooseOptionItemLayoutSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop option: LithoChooseOptionsItem): Component { + fun onCreate( + context: ComponentContext, + @Prop option: LithoChooseOptionsItem): Component { val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) val selectedColor = context.getColor(R.color.colorAccent) val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 12f) - .child(Text.create(context) - .textRes(option.title) - .textSizeRes(R.dimen.font_size_normal) - .typeface(CoreConfig.FONT_MONSERRAT) - .textStyle(Typeface.BOLD) - .textColor(titleColor) - .flexGrow(1f)) - .child(RoundIcon.create(context) - .iconRes(R.drawable.ic_done_white_48dp) - .bgColor(if (option.selected) selectedColor else titleColor) - .bgAlpha(if (option.selected) 200 else 25) - .iconAlpha(if (option.selected) 1f else 0.6f) - .iconColor(if (option.selected) Color.WHITE else titleColor) - .iconSizeRes(R.dimen.toolbar_round_small_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_small_icon_padding) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.START, 12f)) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + Text.create(context) + .textRes(option.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(CoreConfig.FONT_MONSERRAT) + .textStyle(Typeface.BOLD) + .textColor(titleColor) + .flexGrow(1f)) + .child(RoundIcon.create(context) + .iconRes(R.drawable.ic_done_white_48dp) + .bgColor(if (option.selected) selectedColor else titleColor) + .bgAlpha(if (option.selected) 200 else 25) + .iconAlpha(if (option.selected) 1f else 0.6f) + .iconColor(if (option.selected) Color.WHITE else titleColor) + .iconSizeRes(R.dimen.toolbar_round_small_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_small_icon_padding) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.START, 12f)) row.clickHandler(ChooseOptionItemLayout.onItemClick(context)) return row.build() } @@ -71,16 +76,16 @@ abstract class LithoChooseOptionBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val column = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child(getLithoBottomSheetTitle(componentContext).textRes(title())) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child(getLithoBottomSheetTitle(componentContext).textRes(title())) getOptions(componentContext, dialog).forEach { column.child(ChooseOptionItemLayout.create(componentContext) - .option(it) - .onClick { - it.listener() - reset(componentContext.androidContext, dialog) - }) + .option(it) + .onClick { + it.listener() + reset(componentContext.androidContext, dialog) + }) } return column.build() } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt index ab72af34..abdc7a8f 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt @@ -3,7 +3,11 @@ package com.maubis.scarlet.base.support.sheets import android.app.Dialog import android.graphics.Color import android.graphics.Typeface.BOLD -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -12,7 +16,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_OPEN_SANS @@ -20,21 +23,22 @@ import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType class LithoOptionsItem( - val title: Int, - val subtitle: Int, - val content: String = "", - val icon: Int, - val isSelectable: Boolean = false, - val selected: Boolean = false, - val actionIcon: Int = 0, - val visible: Boolean = true, - val listener: () -> Unit) + val title: Int, + val subtitle: Int, + val content: String = "", + val icon: Int, + val isSelectable: Boolean = false, + val selected: Boolean = false, + val actionIcon: Int = 0, + val visible: Boolean = true, + val listener: () -> Unit) @LayoutSpec object OptionItemLayoutSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop option: LithoOptionsItem): Component { + fun onCreate( + context: ComponentContext, + @Prop option: LithoOptionsItem): Component { val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) val subtitleColor = sAppTheme.get(ThemeColorType.HINT_TEXT) val selectedColor = sAppTheme.get(ThemeColorType.ACCENT_TEXT) @@ -45,61 +49,62 @@ object OptionItemLayoutSpec { } val row = Row.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .paddingDip(YogaEdge.VERTICAL, 12f) - .child( - RoundIcon.create(context) - .iconRes(option.icon) - .bgColor(titleColor) - .iconColor(titleColor) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .bgAlpha(15) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.END, 16f)) - .child(Column.create(context) - .flexGrow(1f) - .child( - Text.create(context) - .textRes(option.title) - .textSizeRes(R.dimen.font_size_normal) - .typeface(FONT_MONSERRAT) - .textStyle(BOLD) - .textColor(titleColor)) - .child( - Text.create(context) - .text(subtitle) - .textSizeRes(R.dimen.font_size_small) - .typeface(FONT_OPEN_SANS) - .textColor(subtitleColor))) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + RoundIcon.create(context) + .iconRes(option.icon) + .bgColor(titleColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(15) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.END, 16f)) + .child( + Column.create(context) + .flexGrow(1f) + .child( + Text.create(context) + .textRes(option.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(FONT_MONSERRAT) + .textStyle(BOLD) + .textColor(titleColor)) + .child( + Text.create(context) + .text(subtitle) + .textSizeRes(R.dimen.font_size_small) + .typeface(FONT_OPEN_SANS) + .textColor(subtitleColor))) if (option.isSelectable) { row.child(RoundIcon.create(context) - .iconRes(if (option.actionIcon == 0) R.drawable.ic_done_white_48dp else option.actionIcon) - .bgColor(if (option.selected) selectedColor else titleColor) - .bgAlpha(if (option.selected) 200 else 25) - .iconAlpha(if (option.selected) 1f else 0.6f) - .iconColor(if (option.selected) Color.WHITE else titleColor) - .iconSizeRes(R.dimen.toolbar_round_small_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_small_icon_padding) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.START, 12f)) + .iconRes(if (option.actionIcon == 0) R.drawable.ic_done_white_48dp else option.actionIcon) + .bgColor(if (option.selected) selectedColor else titleColor) + .bgAlpha(if (option.selected) 200 else 25) + .iconAlpha(if (option.selected) 1f else 0.6f) + .iconColor(if (option.selected) Color.WHITE else titleColor) + .iconSizeRes(R.dimen.toolbar_round_small_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_small_icon_padding) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.START, 12f)) } else if (!option.isSelectable && option.actionIcon != 0) { row.child(RoundIcon.create(context) - .iconRes(option.actionIcon) - .bgColor(titleColor) - .bgAlpha(25) - .iconAlpha(0.9f) - .iconColor(titleColor) - .iconSizeRes(R.dimen.toolbar_round_small_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_small_icon_padding) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.START, 12f)) + .iconRes(option.actionIcon) + .bgColor(titleColor) + .bgAlpha(25) + .iconAlpha(0.9f) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_small_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_small_icon_padding) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.START, 12f)) } row.clickHandler(OptionItemLayout.onItemClick(context)) @@ -113,39 +118,41 @@ object OptionItemLayoutSpec { } class LithoLabelOptionsItem( - val title: Int, - val icon: Int, - val visible: Boolean = true, - val listener: () -> Unit) + val title: Int, + val icon: Int, + val visible: Boolean = true, + val listener: () -> Unit) @LayoutSpec object OptionLabelItemLayoutSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop option: LithoLabelOptionsItem): Component { + fun onCreate( + context: ComponentContext, + @Prop option: LithoLabelOptionsItem): Component { val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) val row = Column.create(context) - .widthPercent(100f) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.VERTICAL, 16f) - .child( - RoundIcon.create(context) - .iconRes(option.icon) - .bgColor(titleColor) - .iconColor(titleColor) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .bgAlpha(15) - .onClick { } - .isClickDisabled(true) - .marginDip(YogaEdge.BOTTOM, 4f)) - .child(Text.create(context) - .textRes(option.title) - .textSizeRes(R.dimen.font_size_normal) - .typeface(FONT_MONSERRAT) - .textStyle(BOLD) - .textColor(titleColor)) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 16f) + .child( + RoundIcon.create(context) + .iconRes(option.icon) + .bgColor(titleColor) + .iconColor(titleColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .bgAlpha(15) + .onClick { } + .isClickDisabled(true) + .marginDip(YogaEdge.BOTTOM, 4f)) + .child( + Text.create(context) + .textRes(option.title) + .textSizeRes(R.dimen.font_size_normal) + .typeface(FONT_MONSERRAT) + .textStyle(BOLD) + .textColor(titleColor)) row.clickHandler(OptionItemLayout.onItemClick(context)) return row.build() } @@ -156,7 +163,6 @@ object OptionLabelItemLayoutSpec { } } - abstract class LithoOptionBottomSheet : LithoBottomSheet() { abstract fun title(): Int @@ -164,16 +170,16 @@ abstract class LithoOptionBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { val column = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .child(getLithoBottomSheetTitle(componentContext).textRes(title())) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .child(getLithoBottomSheetTitle(componentContext).textRes(title())) getOptions(componentContext, dialog).forEach { if (it.visible) { column.child(OptionItemLayout.create(componentContext) - .option(it) - .onClick { - it.listener() - }) + .option(it) + .onClick { + it.listener() + }) } } return column.build() diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt index d22754bb..3a906b68 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt @@ -4,12 +4,15 @@ import com.facebook.litho.ClickEvent import com.facebook.litho.Component import com.facebook.litho.ComponentContext import com.facebook.litho.Row -import com.facebook.litho.annotations.* +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetButton @@ -19,19 +22,20 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType object BottomSheetBarSpec { @OnCreateLayout fun onCreate( - context: ComponentContext, - @Prop(resType = ResType.STRING) primaryAction: String, - @Prop(optional = true) isActionNegative: Boolean?, - @Prop(resType = ResType.STRING, optional = true) secondaryAction: String?, - @Prop(resType = ResType.STRING, optional = true) tertiaryAction: String?): Component { + context: ComponentContext, + @Prop(resType = ResType.STRING) primaryAction: String, + @Prop(optional = true) isActionNegative: Boolean?, + @Prop(resType = ResType.STRING, optional = true) secondaryAction: String?, + @Prop(resType = ResType.STRING, optional = true) tertiaryAction: String?): Component { val actionNegative = isActionNegative ?: false val row = Row.create(context) - .alignItems(YogaAlign.CENTER) + .alignItems(YogaAlign.CENTER) if (secondaryAction !== null && secondaryAction.isNotBlank()) { - row.child(Text.create(context) + row.child( + Text.create(context) .text(secondaryAction) .typeface(CoreConfig.FONT_MONSERRAT) .textSizeRes(R.dimen.font_size_large) @@ -43,7 +47,8 @@ object BottomSheetBarSpec { row.child(EmptySpec.create(context).flexGrow(1f)) if (tertiaryAction !== null && tertiaryAction.isNotBlank()) { - row.child(Text.create(context) + row.child( + Text.create(context) .text(tertiaryAction) .typeface(CoreConfig.FONT_MONSERRAT) .textSizeRes(R.dimen.font_size_large) @@ -53,7 +58,8 @@ object BottomSheetBarSpec { .clickHandler(BottomSheetBar.onTertiaryClickEvent(context))) } - row.child(getLithoBottomSheetButton(context) + row.child( + getLithoBottomSheetButton(context) .text(primaryAction) .backgroundRes(if (actionNegative) R.drawable.disabled_rounded_bg else R.drawable.accent_rounded_bg) .clickHandler(BottomSheetBar.onPrimaryClickEvent(context))) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt index c004c1e4..e5171a8a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt @@ -10,7 +10,6 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -19,27 +18,28 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType object CounterChooserSpec { @OnCreateLayout fun onCreate( - context: ComponentContext, - @Prop value: Int, - @Prop minValue: Int, - @Prop maxValue: Int, - @Prop onValueChange: (Int) -> Unit): Component { + context: ComponentContext, + @Prop value: Int, + @Prop minValue: Int, + @Prop maxValue: Int, + @Prop onValueChange: (Int) -> Unit): Component { val row = Row.create(context) - .alignItems(YogaAlign.CENTER) - .child(EmptySpec.create(context).flexGrow(1f)) - .child(bottomBarRoundIcon(context, ToolbarColorConfig()) - .iconRes(R.drawable.icon_less_counter) - .onClick { onValueChange(Math.max(value - 1, minValue)) }) - .child(Text.create(context) - .text(value.toString()) - .typeface(CoreConfig.FONT_MONSERRAT) - .textSizeRes(R.dimen.font_size_xxxlarge) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) - .child(bottomBarRoundIcon(context, ToolbarColorConfig()) - .iconRes(R.drawable.icon_more_counter) - .onClick { onValueChange(Math.min(value + 1, maxValue)) }) - .child(EmptySpec.create(context).flexGrow(1f)) + .alignItems(YogaAlign.CENTER) + .child(EmptySpec.create(context).flexGrow(1f)) + .child(bottomBarRoundIcon(context, ToolbarColorConfig()) + .iconRes(R.drawable.icon_less_counter) + .onClick { onValueChange(Math.max(value - 1, minValue)) }) + .child( + Text.create(context) + .text(value.toString()) + .typeface(CoreConfig.FONT_MONSERRAT) + .textSizeRes(R.dimen.font_size_xxxlarge) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) + .child(bottomBarRoundIcon(context, ToolbarColorConfig()) + .iconRes(R.drawable.icon_more_counter) + .onClick { onValueChange(Math.min(value + 1, maxValue)) }) + .child(EmptySpec.create(context).flexGrow(1f)) return row.build() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt index 33aee1f6..75402277 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -3,71 +3,79 @@ package com.maubis.scarlet.base.support.specs import android.graphics.Color import android.text.Layout import android.text.TextUtils -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType import com.facebook.litho.widget.SolidColor import com.facebook.litho.widget.Text import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType data class GridSectionItem( - val title: Int = 0, - val sectionColor: Int = 0, - val options: List) + val title: Int = 0, + val sectionColor: Int = 0, + val options: List) data class GridSectionOptionItem( - val icon: Int, - val label: Int, - val listener: () -> Unit, - val visible: Boolean = true) + val icon: Int, + val label: Int, + val listener: () -> Unit, + val visible: Boolean = true) @LayoutSpec object GridOptionSpec { @OnCreateLayout fun onCreate( - context: ComponentContext, - @Prop option: GridSectionOptionItem, - @Prop solidSectionColor: Boolean, - @Prop(resType = ResType.COLOR) labelColor: Int, - @Prop(resType = ResType.COLOR) iconColor: Int, - @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, - @Prop(resType = ResType.COLOR) maxLines: Int, - @Prop(resType = ResType.COLOR) sectionColor: Int): Component { + context: ComponentContext, + @Prop option: GridSectionOptionItem, + @Prop solidSectionColor: Boolean, + @Prop(resType = ResType.COLOR) labelColor: Int, + @Prop(resType = ResType.COLOR) iconColor: Int, + @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, + @Prop(resType = ResType.COLOR) maxLines: Int, + @Prop(resType = ResType.COLOR) sectionColor: Int): Component { return Column.create(context) - .alignItems(YogaAlign.CENTER) - .alignContent(YogaAlign.CENTER) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 4f) - .child( - RoundIcon.create(context) - .bgColor(sectionColor) - .iconColor(iconColor) - .iconRes(option.icon) - .iconSizePx(iconSize) - .iconPaddingRes(R.dimen.primary_round_icon_padding) - .iconMarginVerticalRes(R.dimen.toolbar_round_icon_margin_vertical) - .iconMarginHorizontalRes(R.dimen.toolbar_round_icon_margin_horizontal) - .isClickDisabled(true) - .bgAlpha(if (solidSectionColor) 255 else 15) - ) - .child(Text.create(context) - .textRes(option.label) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_MONSERRAT) - .textSizeRes(R.dimen.font_size_small) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 16f) - .minLines(maxLines) - .maxLines(maxLines) - .ellipsize(TextUtils.TruncateAt.END) - .textColor(labelColor)) - .clickHandler(GridOption.onClick(context)) - .build() + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 4f) + .child( + RoundIcon.create(context) + .bgColor(sectionColor) + .iconColor(iconColor) + .iconRes(option.icon) + .iconSizePx(iconSize) + .iconPaddingRes(R.dimen.primary_round_icon_padding) + .iconMarginVerticalRes(R.dimen.toolbar_round_icon_margin_vertical) + .iconMarginHorizontalRes(R.dimen.toolbar_round_icon_margin_horizontal) + .isClickDisabled(true) + .bgAlpha(if (solidSectionColor) 255 else 15) + ) + .child( + Text.create(context) + .textRes(option.label) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_MONSERRAT) + .textSizeRes(R.dimen.font_size_small) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 16f) + .minLines(maxLines) + .maxLines(maxLines) + .ellipsize(TextUtils.TruncateAt.END) + .textColor(labelColor)) + .clickHandler(GridOption.onClick(context)) + .build() } @OnEvent(ClickEvent::class) @@ -80,44 +88,44 @@ object GridOptionSpec { object GridSectionViewSpec { @OnCreateLayout fun onCreate( - context: ComponentContext, - @Prop section: GridSectionItem, - @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, - @Prop(optional = true) numColumns: Int?, - @Prop(optional = true) maxLines: Int?, - @Prop(optional = true) showSeparator: Boolean?): Component { + context: ComponentContext, + @Prop section: GridSectionItem, + @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, + @Prop(optional = true) numColumns: Int?, + @Prop(optional = true) maxLines: Int?, + @Prop(optional = true) showSeparator: Boolean?): Component { val column = Column.create(context) val primaryColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) if (section.title != 0) { column.child( - Text.create(context) - .textRes(section.title) - .typeface(CoreConfig.FONT_MONSERRAT) - .textSizeRes(R.dimen.font_size_normal) - .maxLines(1) - .ellipsize(TextUtils.TruncateAt.END) - .textColor(primaryColor)) + Text.create(context) + .textRes(section.title) + .typeface(CoreConfig.FONT_MONSERRAT) + .textSizeRes(R.dimen.font_size_normal) + .maxLines(1) + .ellipsize(TextUtils.TruncateAt.END) + .textColor(primaryColor)) } val visibleOptions = section.options.filter { it.visible } val getComponentAtIndex: (Int) -> Component = { index -> when { index >= visibleOptions.size -> EmptySpec.create(context) - .flexGrow(1f) - .flexBasisDip(1f) - .build() + .flexGrow(1f) + .flexBasisDip(1f) + .build() else -> GridOption.create(context) - .flexGrow(1f) - .flexBasisDip(1f) - .solidSectionColor(section.sectionColor != 0) - .labelColor(primaryColor) - .maxLines(maxLines ?: 2) - .iconSizePx(iconSize) - .iconColor(if (section.sectionColor == 0) primaryColor else Color.WHITE) - .sectionColor(if (section.sectionColor == 0) primaryColor else section.sectionColor) - .option(visibleOptions[index]) - .build() + .flexGrow(1f) + .flexBasisDip(1f) + .solidSectionColor(section.sectionColor != 0) + .labelColor(primaryColor) + .maxLines(maxLines ?: 2) + .iconSizePx(iconSize) + .iconColor(if (section.sectionColor == 0) primaryColor else Color.WHITE) + .sectionColor(if (section.sectionColor == 0) primaryColor else section.sectionColor) + .option(visibleOptions[index]) + .build() } } @@ -125,7 +133,7 @@ object GridSectionViewSpec { var index = 0 while (true) { val row = Row.create(context) - .widthPercent(100f) + .widthPercent(100f) if (index >= visibleOptions.size) { break } @@ -138,7 +146,8 @@ object GridSectionViewSpec { } if (showSeparator == true) { - column.child(SolidColor.create(context) + column.child( + SolidColor.create(context) .color(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .heightDip(1.5f) .widthDip(196f) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt index 4df970d2..2f3a6445 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/RoundIconSpec.kt @@ -2,8 +2,16 @@ package com.maubis.scarlet.base.support.specs import android.graphics.Color import android.graphics.drawable.Drawable -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.LongClickEvent +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType import com.facebook.litho.widget.Image import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge @@ -13,28 +21,31 @@ import com.maubis.scarlet.base.support.ui.LithoCircleDrawable object RoundIconSpec { @OnCreateLayout fun onCreate( - context: ComponentContext, - @Prop(resType = ResType.DRAWABLE) icon: Drawable, - @Prop(resType = ResType.COLOR) iconColor: Int, - @Prop(resType = ResType.COLOR) bgColor: Int, - @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, - @Prop(resType = ResType.DIMEN_SIZE, optional = true) iconPadding: Int?, - @Prop(resType = ResType.DIMEN_OFFSET, optional = true) iconMarginVertical: Int?, - @Prop(resType = ResType.DIMEN_OFFSET, optional = true) iconMarginHorizontal: Int?, - @Prop(optional = true) iconAlpha: Float?, - @Prop(optional = true) bgAlpha: Int?, - @Prop(optional = true) isClickDisabled: Boolean?, - @Prop(optional = true) isLongClickEnabled: Boolean?, - @Prop(optional = true) showBorder: Boolean?): Component { + context: ComponentContext, + @Prop(resType = ResType.DRAWABLE) icon: Drawable, + @Prop(resType = ResType.COLOR) iconColor: Int, + @Prop(resType = ResType.COLOR) bgColor: Int, + @Prop(resType = ResType.DIMEN_SIZE) iconSize: Int, + @Prop(resType = ResType.DIMEN_SIZE, optional = true) iconPadding: Int?, + @Prop(resType = ResType.DIMEN_OFFSET, optional = true) iconMarginVertical: Int?, + @Prop(resType = ResType.DIMEN_OFFSET, optional = true) iconMarginHorizontal: Int?, + @Prop(optional = true) iconAlpha: Float?, + @Prop(optional = true) bgAlpha: Int?, + @Prop(optional = true) isClickDisabled: Boolean?, + @Prop(optional = true) isLongClickEnabled: Boolean?, + @Prop(optional = true) showBorder: Boolean?): Component { val image = Image.create(context) - .heightPx(iconSize) - .widthPx(iconSize) - .paddingPx(YogaEdge.ALL, iconPadding ?: 0) - .marginPx(YogaEdge.VERTICAL, iconMarginVertical ?: 0) - .marginPx(YogaEdge.HORIZONTAL, iconMarginHorizontal ?: 0) - .drawable(icon.color(iconColor)) - .alpha(iconAlpha ?: 1f) - .background(LithoCircleDrawable(bgColor, bgAlpha ?: Color.alpha(bgColor), showBorder ?: false)) + .heightPx(iconSize) + .widthPx(iconSize) + .paddingPx(YogaEdge.ALL, iconPadding ?: 0) + .marginPx(YogaEdge.VERTICAL, iconMarginVertical ?: 0) + .marginPx(YogaEdge.HORIZONTAL, iconMarginHorizontal ?: 0) + .drawable(icon.color(iconColor)) + .alpha(iconAlpha ?: 1f) + .background( + LithoCircleDrawable( + bgColor, bgAlpha ?: Color.alpha(bgColor), showBorder + ?: false)) if (isClickDisabled === null || !isClickDisabled) { image.clickHandler(RoundIcon.onClickEvent(context)) } @@ -42,9 +53,9 @@ object RoundIconSpec { image.longClickHandler(RoundIcon.onLongClickEvent(context)) } return Column.create(context) - .alignItems(YogaAlign.CENTER) - .child(image) - .build() + .alignItems(YogaAlign.CENTER) + .child(image) + .build() } @OnEvent(ClickEvent::class) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt index 7ae3148d..28ea0f68 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/SpecUtils.kt @@ -12,14 +12,13 @@ import com.facebook.litho.widget.SolidColor import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.support.ui.ThemeColorType object EmptySpec { fun create(context: ComponentContext): SolidColor.Builder { return SolidColor.create(context) - .color(Color.TRANSPARENT) + .color(Color.TRANSPARENT) } } @@ -31,44 +30,42 @@ fun Drawable.color(tint: Int): Drawable { fun separatorSpec(context: ComponentContext): Component.Builder<*> { return SolidColor.create(context) - .alignSelf(YogaAlign.CENTER) - .colorRes(R.color.material_grey_200) - .heightDip(1f) - .widthDip(164f) - .marginDip(YogaEdge.HORIZONTAL, 32f) - .marginDip(YogaEdge.TOP, 16f) - .marginDip(YogaEdge.BOTTOM, 16f) + .alignSelf(YogaAlign.CENTER) + .colorRes(R.color.material_grey_200) + .heightDip(1f) + .widthDip(164f) + .marginDip(YogaEdge.HORIZONTAL, 32f) + .marginDip(YogaEdge.TOP, 16f) + .marginDip(YogaEdge.BOTTOM, 16f) } - data class ToolbarColorConfig( - var toolbarBackgroundColor: Int = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), - var toolbarIconColor: Int = sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) - + var toolbarBackgroundColor: Int = sAppTheme.get(ThemeColorType.TOOLBAR_BACKGROUND), + var toolbarIconColor: Int = sAppTheme.get(ThemeColorType.TOOLBAR_ICON)) fun bottomBarRoundIcon(context: ComponentContext, colorConfig: ToolbarColorConfig): RoundIcon.Builder { return RoundIcon.create(context) - .bgColor(colorConfig.toolbarIconColor) - .iconColor(colorConfig.toolbarIconColor) - .iconSizeRes(R.dimen.toolbar_round_icon_size) - .iconPaddingRes(R.dimen.toolbar_round_icon_padding) - .iconMarginVerticalRes(R.dimen.toolbar_round_icon_margin_vertical) - .iconMarginHorizontalRes(R.dimen.toolbar_round_icon_margin_horizontal) - .bgAlpha(15) + .bgColor(colorConfig.toolbarIconColor) + .iconColor(colorConfig.toolbarIconColor) + .iconSizeRes(R.dimen.toolbar_round_icon_size) + .iconPaddingRes(R.dimen.toolbar_round_icon_padding) + .iconMarginVerticalRes(R.dimen.toolbar_round_icon_margin_vertical) + .iconMarginHorizontalRes(R.dimen.toolbar_round_icon_margin_horizontal) + .bgAlpha(15) } fun bottomBarCard(context: ComponentContext, child: Component, colorConfig: ToolbarColorConfig): Column.Builder { return Column.create(context) - .widthPercent(100f) - .paddingDip(YogaEdge.ALL, 0f) - .backgroundColor(Color.TRANSPARENT) - .child( - Card.create(context) - .widthPercent(100f) - .backgroundColor(Color.TRANSPARENT) - .clippingColor(Color.TRANSPARENT) - .cardBackgroundColor(colorConfig.toolbarBackgroundColor) - .cornerRadiusDip(0f) - .elevationDip(0f) - .content(child)) + .widthPercent(100f) + .paddingDip(YogaEdge.ALL, 0f) + .backgroundColor(Color.TRANSPARENT) + .child( + Card.create(context) + .widthPercent(100f) + .backgroundColor(Color.TRANSPARENT) + .clippingColor(Color.TRANSPARENT) + .cardBackgroundColor(colorConfig.toolbarBackgroundColor) + .cornerRadiusDip(0f) + .elevationDip(0f) + .content(child)) } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt index b68ae4d0..bdec72ed 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/BottomSheetTabletDialog.kt @@ -11,7 +11,7 @@ class BottomSheetTabletDialog(context: Context, theme: Int) : BottomSheetDialog( super.onCreate(savedInstanceState); val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width_for_tablets) window?.setLayout( - if (width > 0) width else ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); + if (width > 0) width else ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt index 9e9c430f..5e0348cd 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/CircleDrawable.kt @@ -1,8 +1,12 @@ package com.maubis.scarlet.base.support.ui -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect import android.graphics.drawable.Drawable -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme class CircleDrawable(color: Int, showBorder: Boolean = true) : Drawable() { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt index 73bef78d..dbe18e11 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/LithoCircleDrawable.kt @@ -1,8 +1,12 @@ package com.maubis.scarlet.base.support.ui -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect import com.facebook.litho.drawable.ComparableDrawable -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean = false) : ComparableDrawable() { @@ -28,10 +32,10 @@ class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean val bounds = bounds canvas.drawCircle(bounds.centerX().toFloat(), bounds.centerY().toFloat(), mRadius.toFloat(), mBorderPaint) canvas.drawCircle( - bounds.centerX().toFloat(), - bounds.centerY().toFloat(), - mRadius.toFloat() - (if (showBorder) 2 else 0), - mPaint) + bounds.centerX().toFloat(), + bounds.centerY().toFloat(), + mRadius.toFloat() - (if (showBorder) 2 else 0), + mPaint) } override fun setAlpha(alpha: Int) { @@ -53,8 +57,8 @@ class LithoCircleDrawable(color: Int, alpha: Int = 255, val showBorder: Boolean override fun isEquivalentTo(other: ComparableDrawable?): Boolean { return other is LithoCircleDrawable - && other.mRadius == mRadius - && other.mPaint.color == mPaint.color + && other.mRadius == mRadius + && other.mPaint.color == mPaint.color } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeColorType.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeColorType.kt index 106813c9..e4961046 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeColorType.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeColorType.kt @@ -1,6 +1,5 @@ package com.maubis.scarlet.base.support.ui - enum class ThemeColorType { BACKGROUND, STATUS_BAR, diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt index 1293a609..e30bc58d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemeManager.kt @@ -75,7 +75,8 @@ class ThemeManager : IThemeManager { } if (map[ThemeColorType.TOOLBAR_BACKGROUND] == map[ThemeColorType.BACKGROUND]) { - map[ThemeColorType.TOOLBAR_BACKGROUND] = ColorUtil.darkerOrSlightlyDarkerColor(map[ThemeColorType.TOOLBAR_BACKGROUND] + map[ThemeColorType.TOOLBAR_BACKGROUND] = ColorUtil.darkerOrSlightlyDarkerColor( + map[ThemeColorType.TOOLBAR_BACKGROUND] ?: 0) } @@ -145,164 +146,164 @@ class ThemeManager : IThemeManager { // NOTE: These names cannot be changed enum class Theme( - val isNightTheme: Boolean, - val background: Int, - val primaryText: Int, - val secondaryText: Int, - val tertiaryText: Int, - val hintText: Int, - val disabledText: Int, - val accentText: Int, - val sectionHeader: Int, - val toolbarBackground: Int, - val toolbarIcon: Int, - val statusBarColorFallback: Int? = null) { + val isNightTheme: Boolean, + val background: Int, + val primaryText: Int, + val secondaryText: Int, + val tertiaryText: Int, + val hintText: Int, + val disabledText: Int, + val accentText: Int, + val sectionHeader: Int, + val toolbarBackground: Int, + val toolbarIcon: Int, + val statusBarColorFallback: Int? = null) { LIGHT( - isNightTheme = false, - background = R.color.white, - primaryText = R.color.dark_primary_text, - secondaryText = R.color.dark_secondary_text, - tertiaryText = R.color.dark_tertiary_text, - hintText = R.color.dark_hint_text, - disabledText = R.color.material_grey_600, - accentText = R.color.colorAccent, - sectionHeader = R.color.material_blue_grey_600, - toolbarBackground = R.color.material_grey_50, - toolbarIcon = R.color.dark_secondary_text, - statusBarColorFallback = R.color.material_grey_500), + isNightTheme = false, + background = R.color.white, + primaryText = R.color.dark_primary_text, + secondaryText = R.color.dark_secondary_text, + tertiaryText = R.color.dark_tertiary_text, + hintText = R.color.dark_hint_text, + disabledText = R.color.material_grey_600, + accentText = R.color.colorAccent, + sectionHeader = R.color.material_blue_grey_600, + toolbarBackground = R.color.material_grey_50, + toolbarIcon = R.color.dark_secondary_text, + statusBarColorFallback = R.color.material_grey_500), OFF_WHITE( - isNightTheme = false, - background = R.color.bg_off_white, - primaryText = R.color.dark_primary_text, - secondaryText = R.color.dark_secondary_text, - tertiaryText = R.color.dark_tertiary_text, - hintText = R.color.dark_hint_text, - disabledText = R.color.material_grey_600, - accentText = R.color.colorAccent, - sectionHeader = R.color.material_blue_grey_700, - toolbarBackground = R.color.bg_off_white_dark, - toolbarIcon = R.color.dark_secondary_text, - statusBarColorFallback = R.color.bg_off_white_darkest), + isNightTheme = false, + background = R.color.bg_off_white, + primaryText = R.color.dark_primary_text, + secondaryText = R.color.dark_secondary_text, + tertiaryText = R.color.dark_tertiary_text, + hintText = R.color.dark_hint_text, + disabledText = R.color.material_grey_600, + accentText = R.color.colorAccent, + sectionHeader = R.color.material_blue_grey_700, + toolbarBackground = R.color.bg_off_white_dark, + toolbarIcon = R.color.dark_secondary_text, + statusBarColorFallback = R.color.bg_off_white_darkest), PEACH( - isNightTheme = false, - background = R.color.bg_peach, - primaryText = R.color.dark_primary_text, - secondaryText = R.color.dark_secondary_text, - tertiaryText = R.color.dark_tertiary_text, - hintText = R.color.dark_hint_text, - disabledText = R.color.material_grey_600, - accentText = R.color.colorAccent, - sectionHeader = R.color.material_blue_grey_700, - toolbarBackground = R.color.bg_peach_dark, - toolbarIcon = R.color.dark_secondary_text, - statusBarColorFallback = R.color.bg_peach_darkest), + isNightTheme = false, + background = R.color.bg_peach, + primaryText = R.color.dark_primary_text, + secondaryText = R.color.dark_secondary_text, + tertiaryText = R.color.dark_tertiary_text, + hintText = R.color.dark_hint_text, + disabledText = R.color.material_grey_600, + accentText = R.color.colorAccent, + sectionHeader = R.color.material_blue_grey_700, + toolbarBackground = R.color.bg_peach_dark, + toolbarIcon = R.color.dark_secondary_text, + statusBarColorFallback = R.color.bg_peach_darkest), ROSE( - isNightTheme = false, - background = R.color.app_theme_rose, - primaryText = R.color.dark_primary_text, - secondaryText = R.color.dark_secondary_text, - tertiaryText = R.color.dark_tertiary_text, - hintText = R.color.dark_hint_text, - disabledText = R.color.material_grey_600, - accentText = R.color.colorAccent, - sectionHeader = R.color.material_blue_grey_700, - toolbarBackground = R.color.app_theme_rose_dark, - toolbarIcon = R.color.dark_secondary_text, - statusBarColorFallback = R.color.app_theme_rose_dark), + isNightTheme = false, + background = R.color.app_theme_rose, + primaryText = R.color.dark_primary_text, + secondaryText = R.color.dark_secondary_text, + tertiaryText = R.color.dark_tertiary_text, + hintText = R.color.dark_hint_text, + disabledText = R.color.material_grey_600, + accentText = R.color.colorAccent, + sectionHeader = R.color.material_blue_grey_700, + toolbarBackground = R.color.app_theme_rose_dark, + toolbarIcon = R.color.dark_secondary_text, + statusBarColorFallback = R.color.app_theme_rose_dark), TEAL( - isNightTheme = true, - background = R.color.app_theme_oceanic, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.app_theme_oceanic, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.app_theme_oceanic, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.app_theme_oceanic, + toolbarIcon = R.color.white), VIOLET( - isNightTheme = true, - background = R.color.app_theme_violet, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.app_theme_violet, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.app_theme_violet, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.app_theme_violet, + toolbarIcon = R.color.white), HONEYSUCKLE( - isNightTheme = true, - background = R.color.app_theme_honeysuckle, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_yellow_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.app_theme_honeysuckle, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.app_theme_honeysuckle, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_yellow_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.app_theme_honeysuckle, + toolbarIcon = R.color.white), BROWN( - isNightTheme = true, - background = R.color.material_brown_800, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.material_brown_900, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.material_brown_800, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.material_brown_900, + toolbarIcon = R.color.white), BLUE_GRAY( - isNightTheme = true, - background = R.color.material_blue_grey_900, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.material_blue_grey_900, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.material_blue_grey_900, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.material_blue_grey_900, + toolbarIcon = R.color.white), DARK( - isNightTheme = true, - background = R.color.material_grey_850, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.material_grey_900, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.material_grey_850, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.material_grey_900, + toolbarIcon = R.color.white), VERY_DARK( - isNightTheme = true, - background = R.color.material_grey_900, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.material_grey_900, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.material_grey_900, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.material_grey_900, + toolbarIcon = R.color.white), BLACK( - isNightTheme = true, - background = R.color.black, - primaryText = R.color.light_primary_text, - secondaryText = R.color.light_primary_text, - tertiaryText = R.color.light_secondary_text, - hintText = R.color.light_hint_text, - disabledText = R.color.material_grey_200, - accentText = R.color.material_pink_accent_100, - sectionHeader = R.color.material_blue_grey_200, - toolbarBackground = R.color.black, - toolbarIcon = R.color.white), + isNightTheme = true, + background = R.color.black, + primaryText = R.color.light_primary_text, + secondaryText = R.color.light_primary_text, + tertiaryText = R.color.light_secondary_text, + hintText = R.color.light_hint_text, + disabledText = R.color.material_grey_200, + accentText = R.color.material_pink_accent_100, + sectionHeader = R.color.material_blue_grey_200, + toolbarBackground = R.color.black, + toolbarIcon = R.color.white), } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt index aec0a50b..731e05c4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.view.View import android.view.inputmethod.InputMethodManager -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.settings.sheet.sInternalEnableFullScreen import com.maubis.scarlet.base.support.utils.OsVersionUtils @@ -50,7 +49,7 @@ abstract class ThemedActivity : AppCompatActivity(), IThemeChangeListener { } window.decorView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_IMMERSIVE + View.SYSTEM_UI_FLAG_IMMERSIVE // Set the content to appear under the system bars so that the // content doesn't resize when the system bars hide and show. or View.SYSTEM_UI_FLAG_LAYOUT_STABLE diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt index 98e817c0..d119cc46 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/ThemedBottomSheetFragment.kt @@ -12,7 +12,6 @@ import android.view.View import com.github.bijoysingh.starter.fragments.SimpleBottomSheetFragment import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme abstract class ThemedBottomSheetFragment : SimpleBottomSheetFragment() { diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt index 8b42231f..61a1be46 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/DateFormatUtils.kt @@ -11,19 +11,19 @@ class DateFormatUtils(context: Context) { private val is24HourFormat = DateFormat.is24HourFormat(context) fun readableFullTime(timestamp: Long): String = readableTime( - DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, - timestamp) + DateFormatter.Formats.HH_MM_A_DD_MMM_YYYY.format, + timestamp) fun readableTime(format: String, timestamp: Long): String { val hourFormatSafe = when { is24HourFormat -> format - .replace("a", "") - .replace("h", "H") + .replace("a", "") + .replace("h", "H") else -> format } return DateFormatter.getDate( - DateFormat.getBestDateTimePattern(Locale.getDefault(), hourFormatSafe), - timestamp) + DateFormat.getBestDateTimePattern(Locale.getDefault(), hourFormatSafe), + timestamp) } fun getDateForBackup(): String = DateFormatter.getDate("dd_MMM_yyyy", Calendar.getInstance()) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt index 7e185d7b..b8968672 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ExceptionUtils.kt @@ -49,7 +49,6 @@ fun maybeThrow(activity: AppCompatActivity, thrownException: Exception) { maybeThrow(thrownException) } - /** * Throws in debug builds and stores the log trace to a fixed note in case of 'internal debug mode'. */ @@ -100,7 +99,6 @@ fun throwOrReturn(exception: Exception, result: DataType): DataType { return result } - private fun storeToDebugNote(trace: String) { GlobalScope.launch { storeToDebugNoteSync(trace) @@ -114,10 +112,10 @@ const val EXCEPTION_NOTE_MAX_EXCEPTIONS = 20 @Synchronized private fun storeToDebugNoteSync(trace: String) { val note = instance.notesDatabase().getByUUID(EXCEPTION_NOTE_KEY) - ?: NoteBuilder().emptyNote().apply { - uuid = EXCEPTION_NOTE_KEY - disableBackup = true - } + ?: NoteBuilder().emptyNote().apply { + uuid = EXCEPTION_NOTE_KEY + disableBackup = true + } val initialFormats = note.getFormats().toMutableList() if (note.isUnsaved() || initialFormats.isEmpty()) { @@ -126,10 +124,12 @@ private fun storeToDebugNoteSync(trace: String) { val additionalFormats = emptyList().toMutableList() additionalFormats.add(Format(FormatType.SUB_HEADING, "Exception")) - additionalFormats.add(Format( + additionalFormats.add( + Format( FormatType.QUOTE, "Throw at ${DateFormatter.getDate(System.currentTimeMillis())}")) - additionalFormats.add(Format( + additionalFormats.add( + Format( FormatType.CODE, trace)) additionalFormats.add(Format(FormatType.SEPARATOR)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt index a43ef511..21bed475 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference - enum class Flavor { NONE, // FDroid, Master Builds LITE, // Play Store Version diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt index a494eefc..8ba8d59c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/ImageCache.kt @@ -29,7 +29,6 @@ class ImageCache(context: Context) { } } - fun imagesForNote(noteUUID: String): Array { val folder = File(persistentFolder, noteUUID) return folder.listFiles() ?: emptyArray() diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/TextInputUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/TextInputUtils.kt index 9e130012..40984622 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/TextInputUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/TextInputUtils.kt @@ -5,8 +5,8 @@ import android.view.inputmethod.EditorInfo import android.widget.TextView fun getEditorActionListener( - runnable: () -> Boolean, - preConditions: () -> Boolean = { false }): TextView.OnEditorActionListener { + runnable: () -> Boolean, + preConditions: () -> Boolean = { false }): TextView.OnEditorActionListener { return TextView.OnEditorActionListener { _: TextView, actionId: Int, event: KeyEvent? -> if (preConditions()) { return@OnEditorActionListener false diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt index 94259917..6bea70f6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetProvider.kt @@ -26,8 +26,8 @@ class AllNotesWidgetProvider : AppWidgetProvider() { val appWidgetId = appWidgetIds[i] val views = RemoteViews( - context.packageName, - R.layout.widget_layout_all_notes + context.packageName, + R.layout.widget_layout_all_notes ) val intent = Intent(context, AllNotesWidgetService::class.java) intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]) @@ -62,7 +62,7 @@ class AllNotesWidgetProvider : AppWidgetProvider() { fun notifyAllChanged(context: Context) { val application: Application = context.applicationContext as Application val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( - ComponentName(application, AllNotesWidgetProvider::class.java)) + ComponentName(application, AllNotesWidgetProvider::class.java)) if (ids.isEmpty()) { return } diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt index 50a39605..1a952904 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/AllNotesWidgetService.kt @@ -14,7 +14,6 @@ import com.maubis.scarlet.base.support.ui.ColorUtil import com.maubis.scarlet.base.widget.sheet.getWidgetNoteText import com.maubis.scarlet.base.widget.sheet.getWidgetNotes - class AllNotesWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { return AllNotesRemoteViewsFactory(applicationContext) @@ -33,7 +32,7 @@ class AllNotesRemoteViewsFactory(val context: Context) : RemoteViewsService.Remo } override fun getItemId(position: Int): Long { - return if(position < notes.size) notes[position].uid.toLong() else 0 + return if (position < notes.size) notes[position].uid.toLong() else 0 } override fun onDataSetChanged() { diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt b/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt index 5bfeeade..2cb08fa7 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/CreateNoteWidgetProvider.kt @@ -12,7 +12,6 @@ import com.maubis.scarlet.base.R import com.maubis.scarlet.base.note.creation.activity.CreateListNoteActivity import com.maubis.scarlet.base.note.creation.activity.CreateNoteActivity - class CreateNoteWidgetProvider : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { @@ -39,10 +38,11 @@ class CreateNoteWidgetProvider : AppWidgetProvider() { } } -fun getPendingIntentWithStack(context: Context, requestCode: Int, resultIntent: Intent, flags: Int = PendingIntent.FLAG_UPDATE_CURRENT): PendingIntent { +fun getPendingIntentWithStack( + context: Context, requestCode: Int, resultIntent: Intent, flags: Int = PendingIntent.FLAG_UPDATE_CURRENT): PendingIntent { return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(Intent(context, MainActivity::class.java)) - .addNextIntent(resultIntent) - .getPendingIntent(requestCode, flags) - ?: PendingIntent.getActivity(context, requestCode, resultIntent, 0) + .addNextIntentWithParentStack(Intent(context, MainActivity::class.java)) + .addNextIntent(resultIntent) + .getPendingIntent(requestCode, flags) + ?: PendingIntent.getActivity(context, requestCode, resultIntent, 0) } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt index 7cc3f70a..35005351 100644 --- a/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/widget/sheet/WidgetOptionsBottomSheet.kt @@ -83,7 +83,7 @@ fun getWidgetNotes(): List { val sorting = SortingOptionsBottomSheet.getSortingState() return sort(CoreConfig.notesDb.getByNoteState(state.toTypedArray()) - .filter { note -> (!note.locked || sWidgetShowLockedNotes) }, sorting) + .filter { note -> (!note.locked || sWidgetShowLockedNotes) }, sorting) } class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { @@ -92,7 +92,8 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { override fun getOptions(componentContext: ComponentContext, dialog: Dialog): List { val activity = context as MainActivity val options = ArrayList() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.widget_option_enable_formatting, subtitle = R.string.widget_option_enable_formatting_details, icon = R.drawable.ic_markdown_logo, @@ -103,8 +104,9 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sWidgetEnableFormatting - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.widget_option_show_locked_notes, subtitle = R.string.widget_option_show_locked_notes_details, icon = R.drawable.ic_action_lock, @@ -115,8 +117,9 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sWidgetShowLockedNotes - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.widget_option_show_archived_notes, subtitle = R.string.widget_option_show_archived_notes_details, icon = R.drawable.ic_archive_white_48dp, @@ -127,8 +130,9 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sWidgetShowArchivedNotes - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.widget_option_show_trash_notes, subtitle = R.string.widget_option_show_trash_notes_details, icon = R.drawable.icon_delete, @@ -139,8 +143,9 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sWidgetShowDeletedNotes - )) - options.add(LithoOptionsItem( + )) + options.add( + LithoOptionsItem( title = R.string.widget_option_show_toolbar, subtitle = R.string.widget_option_show_toolbar_details, icon = R.drawable.ic_action_grid, @@ -151,10 +156,11 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { }, isSelectable = true, selected = sWidgetShowToolbar - )) + )) val isLite = FlavorUtils.isLite() - options.add(LithoOptionsItem( + options.add( + LithoOptionsItem( title = R.string.widget_option_background_color, subtitle = R.string.widget_option_background_color_details, icon = R.drawable.ic_action_color, @@ -166,18 +172,18 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { openSheet(activity, ColorPickerBottomSheet().apply { config = ColorPickerDefaultController( - title = R.string.widget_option_background_color, - selectedColor = sWidgetBackgroundColor, - colors = listOf(intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.LTGRAY, 0x65000000, Color.DKGRAY, Color.BLACK)), - onColorSelected = { - sWidgetBackgroundColor = it - notifyAllNotesConfigChanged(activity) - }, - columns = 6) + title = R.string.widget_option_background_color, + selectedColor = sWidgetBackgroundColor, + colors = listOf(intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.LTGRAY, 0x65000000, Color.DKGRAY, Color.BLACK)), + onColorSelected = { + sWidgetBackgroundColor = it + notifyAllNotesConfigChanged(activity) + }, + columns = 6) }) }, actionIcon = if (!isLite) 0 else R.drawable.ic_rating - )) + )) return options } @@ -186,7 +192,7 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { val singleNoteBroadcastIntent = GlobalScope.async { val application: Application = activity.applicationContext as Application val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( - ComponentName(application, NoteWidgetProvider::class.java)) + ComponentName(application, NoteWidgetProvider::class.java)) val intent = Intent(application, NoteWidgetProvider::class.java) intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE @@ -204,7 +210,7 @@ class WidgetOptionsBottomSheet : LithoOptionBottomSheet() { val allNotesBroadcastIntent = GlobalScope.async { val application: Application = activity.applicationContext as Application val ids = AppWidgetManager.getInstance(application).getAppWidgetIds( - ComponentName(application, AllNotesWidgetProvider::class.java)) + ComponentName(application, AllNotesWidgetProvider::class.java)) val intent = Intent(application, AllNotesWidgetProvider::class.java) intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE diff --git a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt index ff80f8cb..91851098 100644 --- a/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt +++ b/markdown/src/main/java/com/maubis/markdown/inliner/TextInliner.kt @@ -1,6 +1,5 @@ package com.maubis.markdown.inliner -import android.util.Log import com.maubis.markdown.MarkdownConfig.Companion.config class TextInliner(val text: String) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt index f86c45d9..6b335a93 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteController.kt @@ -14,7 +14,15 @@ import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.config.auth.IPendingUploadListener import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.database.remote.IRemoteDatabaseUtils -import com.maubis.scarlet.base.export.data.* +import com.maubis.scarlet.base.export.data.ExportableFolder +import com.maubis.scarlet.base.export.data.ExportableNoteMeta +import com.maubis.scarlet.base.export.data.ExportableTag +import com.maubis.scarlet.base.export.data.fromExportedMarkdown +import com.maubis.scarlet.base.export.data.getExportableFolder +import com.maubis.scarlet.base.export.data.getExportableNoteMeta +import com.maubis.scarlet.base.export.data.getExportableTag +import com.maubis.scarlet.base.export.data.mergeMetas +import com.maubis.scarlet.base.export.data.toExportedMarkdown import com.maubis.scarlet.base.note.creation.sheet.sNoteDefaultColor import com.maubis.scarlet.base.support.utils.log import com.maubis.scarlet.base.support.utils.maybeThrow @@ -24,7 +32,6 @@ import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean - const val FOLDER_NAME_IMAGES = "images" const val FOLDER_NAME_NOTES = "notes" const val FOLDER_NAME_NOTES_META = "notes_meta" @@ -124,6 +131,7 @@ abstract class RemoteController(private val */ abstract fun initSyncs() + abstract fun getResourceId(data: RemoteUploadData): ResourceId abstract fun getResourceIdForFolderName(folderName: String): ResourceId? abstract fun storeResourceIdForFolderName(folderName: String, resource: ResourceId) @@ -420,7 +428,7 @@ abstract class RemoteController(private val try { val itemDescription = fromExportedMarkdown(content) val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) - ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } + ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } val temporaryNote = NoteBuilder().copy(existingNote) temporaryNote.description = itemDescription IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) @@ -438,7 +446,7 @@ abstract class RemoteController(private val val item = Gson().fromJson(content, ExportableNoteMeta::class.java) val existingNote = CoreConfig.notesDb.getByUUID(data.uuid) - ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } + ?: NoteBuilder().emptyNote(sNoteDefaultColor).apply { uuid = data.uuid } val temporaryNote = NoteBuilder().copy(existingNote) temporaryNote.mergeMetas(item) IRemoteDatabaseUtils.onRemoteInsert(context, temporaryNote.getFirebaseNote()) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt index 4f94969f..dd3061ec 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteDatabaseStateController.kt @@ -52,7 +52,7 @@ class RemoteDatabaseStateController(context: Context) : IRemoteDatabaseState { } data is Note -> { if (database.getByUUID(RemoteDataType.NOTE.name, data.uuid) === null - || database.getByUUID(RemoteDataType.NOTE_META.name, data.uuid) === null) { + || database.getByUUID(RemoteDataType.NOTE_META.name, data.uuid) === null) { notifyInsert(data) {} } } @@ -75,16 +75,16 @@ class RemoteDatabaseStateController(context: Context) : IRemoteDatabaseState { notifyImageIds(note) { imageUUIDs.add(it) } database.getByType(RemoteDataType.IMAGE.name) - .filter { - val uuid = toImageUUID(it.uuid) - uuid?.noteUuid == note.uuid && !imageUUIDs.contains(uuid) - }.forEach { - it.apply { - lastUpdateTimestamp = getTrueCurrentTime() - localStateDeleted = true - save(database) - } + .filter { + val uuid = toImageUUID(it.uuid) + uuid?.noteUuid == note.uuid && !imageUUIDs.contains(uuid) + }.forEach { + it.apply { + lastUpdateTimestamp = getTrueCurrentTime() + localStateDeleted = true + save(database) } + } imageUUIDs.forEach { val existing = database.getByUUID(RemoteDataType.IMAGE.name, it.name()) @@ -173,10 +173,10 @@ class RemoteDatabaseStateController(context: Context) : IRemoteDatabaseState { } fun localDatabaseUpdate( - itemType: RemoteDataType, - itemUUID: String, - onExecution: () -> Unit, - removed: Boolean = false) { + itemType: RemoteDataType, + itemUUID: String, + onExecution: () -> Unit, + removed: Boolean = false) { GlobalScope.launch { val database = remoteDatabase if (database === null) { @@ -197,9 +197,9 @@ class RemoteDatabaseStateController(context: Context) : IRemoteDatabaseState { } fun localDatabaseRemove( - itemType: RemoteDataType, - itemUUID: String, - onExecution: () -> Unit) { + itemType: RemoteDataType, + itemUUID: String, + onExecution: () -> Unit) { GlobalScope.launch { val database = remoteDatabase if (database !== null) { @@ -214,9 +214,9 @@ class RemoteDatabaseStateController(context: Context) : IRemoteDatabaseState { private fun notifyImageIds(note: Note, onImageUUID: (ImageUUID) -> Unit) { val imageIds = note.getFormats() - .filter { it.formatType == FormatType.IMAGE } - .map { it.text } - .toSet() + .filter { it.formatType == FormatType.IMAGE } + .map { it.text } + .toSet() imageIds.forEach { onImageUUID(ImageUUID(note.uuid, it)) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt index f4318663..471a1fa4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadData.kt @@ -1,6 +1,15 @@ package com.bijoysingh.quicknote.database -import android.arch.persistence.room.* +import android.arch.persistence.room.ColumnInfo +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Delete +import android.arch.persistence.room.Entity +import android.arch.persistence.room.Ignore +import android.arch.persistence.room.Index +import android.arch.persistence.room.Insert +import android.arch.persistence.room.OnConflictStrategy +import android.arch.persistence.room.PrimaryKey +import android.arch.persistence.room.Query import com.google.gson.Gson import com.maubis.scarlet.base.support.utils.log import com.maubis.scarlet.base.support.utils.maybeThrow @@ -28,10 +37,10 @@ class RemoteUploadData { var localStateDeleted: Boolean = false - @ColumnInfo(name="gDriveUpdateTimestamp") + @ColumnInfo(name = "gDriveUpdateTimestamp") var remoteUpdateTimestamp: Long = 0L - @ColumnInfo(name="gDriveStateDeleted") + @ColumnInfo(name = "gDriveStateDeleted") var remoteStateDeleted: Boolean = false var attempts: Long = 0L @@ -66,7 +75,8 @@ object RemoteDatabaseHelper { this.uuid = itemUuid this.type = itemType } - }} + } +} @Dao interface RemoteUploadDataDao { @@ -82,34 +92,40 @@ interface RemoteUploadDataDao { @Delete fun delete(note: RemoteUploadData) - @Query("SELECT * " + + @Query( + "SELECT * " + "FROM gdrive_upload " + "WHERE uid = :uid " + "LIMIT 1") fun getByID(uid: Int): RemoteUploadData? - @Query("SELECT * " + + @Query( + "SELECT * " + "FROM gdrive_upload " + "WHERE uuid = :uuid AND type = :type " + "LIMIT 1") fun getByUUID(type: String, uuid: String): RemoteUploadData? - @Query("SELECT * " + + @Query( + "SELECT * " + "FROM gdrive_upload " + "WHERE type = :type") fun getByType(type: String): List - @Query("SELECT COUNT(*) " + + @Query( + "SELECT COUNT(*) " + "FROM gdrive_upload " + "WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") fun getPendingCount(): Int - @Query("SELECT * " + + @Query( + "SELECT * " + "FROM gdrive_upload " + "WHERE (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") fun getAllPending(): List - @Query("SELECT * " + + @Query( + "SELECT * " + "FROM gdrive_upload " + "WHERE type = :type AND (lastUpdateTimestamp != gDriveUpdateTimestamp OR localStateDeleted != gDriveStateDeleted)") fun getPendingByType(type: String): List diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt index 17e1a42f..e5241500 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/database/RemoteUploadDatabase.kt @@ -9,8 +9,8 @@ var remoteDatabase: RemoteUploadDataDao? = null fun genRemoteDatabase(context: Context): RemoteUploadDataDao? { if (remoteDatabase === null) { remoteDatabase = Room.databaseBuilder(context, RemoteUploadDatabase::class.java, "google_drive_db") - .fallbackToDestructiveMigration() - .build().remote() + .fallbackToDestructiveMigration() + .build().remote() } return remoteDatabase } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index 8a131172..016baae8 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -4,15 +4,22 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.text.Layout import com.bijoysingh.quicknote.scarlet.sFirebaseKilled -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType import com.facebook.litho.widget.Image import com.facebook.litho.widget.Text import com.facebook.litho.widget.VerticalScroll import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.color @@ -22,49 +29,54 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType @LayoutSpec object GDriveRootViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop loggingIn: Boolean): Component { + fun onCreate( + context: ComponentContext, + @Prop loggingIn: Boolean): Component { val buttonTitle = when { loggingIn -> R.string.google_drive_page_logging_in_button else -> R.string.google_drive_page_login_button } return Column.create(context) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(VerticalScroll.create(context) - .flexGrow(1f) - .marginDip(YogaEdge.ALL, 8f) - .childComponent(GDriveContentView.create(context))) - .child(Text.create(context) - .backgroundRes(R.drawable.secondary_rounded_bg) - .marginDip(YogaEdge.HORIZONTAL, 16f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .textRes(R.string.google_drive_page_login_firebase_button) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_MONSERRAT) - .alpha(if (sFirebaseKilled) 0.5f else 1f) - .clickHandler(GDriveRootView.onFirebaseClick(context))) - .child(Row.create(context) - .backgroundRes(R.drawable.accent_rounded_bg) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .marginDip(YogaEdge.ALL, 16f) - .child( - Image.create(context) - .drawableRes(R.drawable.gdrive_icon) - .heightDip(36f) - ) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColorRes(R.color.white) - .textRes(buttonTitle) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) - .clickHandler(GDriveRootView.onGoogleClickEvent(context))) - .build() + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(GDriveContentView.create(context))) + .child( + Text.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .textSizeRes(R.dimen.font_size_large) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.google_drive_page_login_firebase_button) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .typeface(CoreConfig.FONT_MONSERRAT) + .alpha(if (sFirebaseKilled) 0.5f else 1f) + .clickHandler(GDriveRootView.onFirebaseClick(context))) + .child( + Row.create(context) + .backgroundRes(R.drawable.accent_rounded_bg) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.gdrive_icon) + .heightDip(36f) + ) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(GDriveRootView.onGoogleClickEvent(context))) + .build() } @OnEvent(ClickEvent::class) @@ -83,53 +95,60 @@ object GDriveContentViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { return Column.create(context) - .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_xxlarge) - .textRes(R.string.google_drive_page_login_title) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .textRes(R.string.google_drive_page_login_details) - .typeface(CoreConfig.FONT_MONSERRAT)) - .child(GDriveIconView.create(context) - .marginDip(YogaEdge.TOP, 24f) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_action_lock) - .titleRes(R.string.google_drive_page_login_lock_details)) - .child(GDriveIconView.create(context) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_image_gallery) - .titleRes(R.string.google_drive_page_photo_details)) - .build() + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.google_drive_page_login_title) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.google_drive_page_login_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child( + GDriveIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_action_lock) + .titleRes(R.string.google_drive_page_login_lock_details)) + .child( + GDriveIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_image_gallery) + .titleRes(R.string.google_drive_page_photo_details)) + .build() } } @LayoutSpec object GDriveIconViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop(resType = ResType.COLOR) bgColor: Int, - @Prop(resType = ResType.DRAWABLE) icon: Drawable, - @Prop(resType = ResType.STRING) title: String): Component { + fun onCreate( + context: ComponentContext, + @Prop(resType = ResType.COLOR) bgColor: Int, + @Prop(resType = ResType.DRAWABLE) icon: Drawable, + @Prop(resType = ResType.STRING) title: String): Component { return Column.create(context) - .paddingDip(YogaEdge.HORIZONTAL, 32f) - .paddingDip(YogaEdge.VERTICAL, 24f) - .child(Image.create(context) - .drawable(icon.color(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) - .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) - .paddingDip(YogaEdge.ALL, 12f) - .marginDip(YogaEdge.BOTTOM, 12f) - .heightDip(64f)) - .child(Text.create(context) - .text(title) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .textSizeRes(R.dimen.font_size_normal) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT)) - .build() + .paddingDip(YogaEdge.HORIZONTAL, 32f) + .paddingDip(YogaEdge.VERTICAL, 24f) + .child( + Image.create(context) + .drawable(icon.color(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) + .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) + .paddingDip(YogaEdge.ALL, 12f) + .marginDip(YogaEdge.BOTTOM, 12f) + .heightDip(64f)) + .child( + Text.create(context) + .text(title) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .textSizeRes(R.dimen.font_size_normal) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT)) + .build() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt index ee90fad4..1b937b57 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLoginActivity.kt @@ -38,7 +38,6 @@ import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.atomic.AtomicBoolean - // TODO: This is not ready... Recent changes in Drive API make this sh*t a little difficult and // inconclusive. I want to do this because it's safer than Firebase, but f*ck Google for // changing the API So much @@ -81,17 +80,17 @@ class GDriveLoginActivity : SecuredActivity(), GoogleApiClient.OnConnectionFaile private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder( - GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestScopes(Scope(DriveScopes.DRIVE_FILE)) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() mGoogleApiClient = GoogleApiClient - .Builder(this) - .enableAutoManage(this, this) - .addApi(Auth.GOOGLE_SIGN_IN_API, gso) - .build() + .Builder(this) + .enableAutoManage(this, this) + .addApi(Auth.GOOGLE_SIGN_IN_API, gso) + .build() } private fun signIn() { @@ -125,18 +124,18 @@ class GDriveLoginActivity : SecuredActivity(), GoogleApiClient.OnConnectionFaile private fun setButton(state: Boolean) { loggingIn.set(state) component = GDriveRootView.create(componentContext) - .onClick { - if (!loggingIn.get()) { - setButton(true) - signIn() - } - } - .onFirebaseClick { - context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) - finish() + .onClick { + if (!loggingIn.get()) { + setButton(true) + signIn() } - .loggingIn(state) - .build() + } + .onFirebaseClick { + context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) + finish() + } + .loggingIn(state) + .build() setContentView(LithoView.create(componentContext, component)) } @@ -167,15 +166,15 @@ class GDriveLoginActivity : SecuredActivity(), GoogleApiClient.OnConnectionFaile companion object { fun getDriveHelper(context: Context, account: GoogleSignInAccount): GDriveServiceHelper { val credential = GoogleAccountCredential.usingOAuth2( - context, - Collections.singleton(DriveScopes.DRIVE_FILE)) + context, + Collections.singleton(DriveScopes.DRIVE_FILE)) credential.selectedAccount = account.account val googleDriveService = Drive.Builder( - AndroidHttp.newCompatibleTransport(), - GsonFactory(), - credential) - .setApplicationName(context.getString(R.string.app_name)) - .build() + AndroidHttp.newCompatibleTransport(), + GsonFactory(), + credential) + .setApplicationName(context.getString(R.string.app_name)) + .build() return GDriveServiceHelper(googleDriveService) } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt index b828841d..3e7295e6 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivity.kt @@ -39,17 +39,17 @@ class GDriveLogoutActivity : ThemedActivity(), GoogleApiClient.OnConnectionFaile private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder( - GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestScopes(Scope(DriveScopes.DRIVE_FILE)) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() mGoogleApiClient = GoogleApiClient - .Builder(this) - .enableAutoManage(this, this) - .addApi(Auth.GOOGLE_SIGN_IN_API, gso) - .build() + .Builder(this) + .enableAutoManage(this, this) + .addApi(Auth.GOOGLE_SIGN_IN_API, gso) + .build() } private fun signOut() { @@ -69,14 +69,14 @@ class GDriveLogoutActivity : ThemedActivity(), GoogleApiClient.OnConnectionFaile private fun setButton(state: Boolean) { signingOut.set(state) component = GDriveLogoutRootView.create(componentContext) - .onClick { - if (!signingOut.get()) { - setButton(true) - signOut() - } + .onClick { + if (!signingOut.get()) { + setButton(true) + signOut() } - .loggingIn(state) - .build() + } + .loggingIn(state) + .build() setContentView(LithoView.create(componentContext, component)) } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt index 81d2277a..18ee9409 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt @@ -1,7 +1,11 @@ package com.bijoysingh.quicknote.drive import android.text.Layout -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -12,7 +16,6 @@ import com.facebook.litho.widget.VerticalScroll import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -20,38 +23,42 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType @LayoutSpec object GDriveLogoutRootViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop loggingIn: Boolean): Component { + fun onCreate( + context: ComponentContext, + @Prop loggingIn: Boolean): Component { val buttonTitle = when { loggingIn -> R.string.google_drive_page_logging_out_button else -> R.string.google_drive_page_logout_button } return Column.create(context) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(VerticalScroll.create(context) - .flexGrow(1f) - .marginDip(YogaEdge.ALL, 8f) - .childComponent(GDriveLogoutContentView.create(context))) - .child(Row.create(context) - .backgroundRes(R.drawable.login_button_disabled) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .marginDip(YogaEdge.ALL, 16f) - .child( - Image.create(context) - .drawableRes(R.drawable.gdrive_icon) - .heightDip(36f) - ) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColorRes(R.color.white) - .textRes(buttonTitle) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) - .clickHandler(GDriveLogoutRootView.onLogoutClick(context))) - .build() + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(GDriveLogoutContentView.create(context))) + .child( + Row.create(context) + .backgroundRes(R.drawable.login_button_disabled) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.gdrive_icon) + .heightDip(36f) + ) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(GDriveLogoutRootView.onLogoutClick(context))) + .build() } @OnEvent(ClickEvent::class) @@ -65,28 +72,32 @@ object GDriveLogoutContentViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { return Column.create(context) - .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_xxlarge) - .textRes(R.string.google_drive_page_logout_title) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .textRes(R.string.google_drive_page_logout_details) - .typeface(CoreConfig.FONT_MONSERRAT)) - .child(GDriveIconView.create(context) - .marginDip(YogaEdge.TOP, 24f) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.icon_sync_disabled) - .titleRes(R.string.google_drive_page_logout_no_sync_details)) - .child(GDriveIconView.create(context) - .marginDip(YogaEdge.TOP, 16f) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_restore) - .titleRes(R.string.google_drive_page_logout_data_persists_details)) - .build() + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.google_drive_page_logout_title) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.google_drive_page_logout_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child( + GDriveIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.icon_sync_disabled) + .titleRes(R.string.google_drive_page_logout_no_sync_details)) + .child( + GDriveIconView.create(context) + .marginDip(YogaEdge.TOP, 16f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_restore) + .titleRes(R.string.google_drive_page_logout_data_persists_details)) + .build() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index 14aa0400..158bc32e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -6,8 +6,16 @@ import android.text.TextUtils import com.bijoysingh.quicknote.database.RemoteDataType import com.bijoysingh.quicknote.database.RemoteUploadData import com.bijoysingh.quicknote.database.remoteDatabase -import com.facebook.litho.* -import com.facebook.litho.annotations.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType import com.facebook.litho.widget.Image import com.facebook.litho.widget.SolidColor import com.facebook.litho.widget.Text @@ -15,7 +23,6 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.google.gson.Gson import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig @@ -32,41 +39,45 @@ import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean data class PendingItem( - val state: RemoteUploadData, - val info: String? + val state: RemoteUploadData, + val info: String? ) @LayoutSpec object PendingItemIconSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop(resType = ResType.STRING) label: String, - @Prop(resType = ResType.DRAWABLE) icon: Drawable): Component { + fun onCreate( + context: ComponentContext, + @Prop(resType = ResType.STRING) label: String, + @Prop(resType = ResType.DRAWABLE) icon: Drawable): Component { val secondaryColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) return Row.create(context) - .paddingDip(YogaEdge.HORIZONTAL, 8f) - .paddingDip(YogaEdge.VERTICAL, 4f) - .alignItems(YogaAlign.CENTER) - .alignContent(YogaAlign.CENTER) - .backgroundRes(R.drawable.secondary_rounded_bg) - .child(Image.create(context) - .drawable(icon.color(secondaryColor)) - .marginDip(YogaEdge.HORIZONTAL, 4f) - .heightDip(24f)) - .child(Text.create(context) - .text(label) - .textSizeRes(R.dimen.font_size_xsmall) - .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) - .textColor(secondaryColor)) - .build() + .paddingDip(YogaEdge.HORIZONTAL, 8f) + .paddingDip(YogaEdge.VERTICAL, 4f) + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .backgroundRes(R.drawable.secondary_rounded_bg) + .child( + Image.create(context) + .drawable(icon.color(secondaryColor)) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .heightDip(24f)) + .child( + Text.create(context) + .text(label) + .textSizeRes(R.dimen.font_size_xsmall) + .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .textColor(secondaryColor)) + .build() } } @LayoutSpec object PendingItemLayoutSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop option: PendingItem): Component { + fun onCreate( + context: ComponentContext, + @Prop option: PendingItem): Component { val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) val subtitleColor = sAppTheme.get(ThemeColorType.TERTIARY_TEXT) val hintColor = sAppTheme.get(ThemeColorType.HINT_TEXT) @@ -112,99 +123,106 @@ object PendingItemLayoutSpec { } val column = Column.create(context) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 12f) - .child( - Row.create(context) - .paddingDip(YogaEdge.ALL, 2f) - .alignItems(YogaAlign.CENTER) - .child( - PendingItemIcon.create(context) - .iconRes(icon) - .label(label) - .marginDip(YogaEdge.END, 16f)) - .child( - Text.create(context) - .text(uuid) - .maxLines(1) - .ellipsize(TextUtils.TruncateAt.END) - .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) - .textColor(subtitleColor))) - .child( + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 12f) + .child( + Row.create(context) + .paddingDip(YogaEdge.ALL, 2f) + .alignItems(YogaAlign.CENTER) + .child( + PendingItemIcon.create(context) + .iconRes(icon) + .label(label) + .marginDip(YogaEdge.END, 16f)) + .child( Text.create(context) - .text(info) - .maxLines(5) - .paddingDip(YogaEdge.ALL, 8f) - .marginDip(YogaEdge.ALL, 8f) - .ellipsize(TextUtils.TruncateAt.MIDDLE) - .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_MONSERRAT) - .backgroundRes(R.drawable.pending_note_background) - .textColor(subtitleColor)) - .child( - Row.create(context) - .widthPercent(100f) - .paddingDip(YogaEdge.ALL, 4f) - .child(Text.create(context) - .textRes(R.string.pending_backup_local_state) - .textSizeRes(R.dimen.font_size_small) - .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) - .textColor(hintColor)) - .child(Text.create(context) - .textRes(localState) - .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) - .paddingDip(YogaEdge.HORIZONTAL, 6f) - .paddingDip(YogaEdge.VERTICAL, 2f) - .marginDip(YogaEdge.HORIZONTAL, 4f) - .backgroundRes(R.drawable.pending_note_background) - .textColor(hintColor)) - .child(Text.create(context) - .text(localUpdateTime) - .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) - .paddingDip(YogaEdge.HORIZONTAL, 6f) - .paddingDip(YogaEdge.VERTICAL, 2f) - .backgroundRes(R.drawable.pending_note_background) - .textColor(hintColor)) - ) - .child( - Row.create(context) - .widthPercent(100f) - .paddingDip(YogaEdge.ALL, 4f) - .child(Text.create(context) - .textRes(R.string.pending_backup_remote_state) - .textSizeRes(R.dimen.font_size_small) - .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) - .textColor(hintColor)) - .child(Text.create(context) - .textRes(remoteState) - .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) - .paddingDip(YogaEdge.HORIZONTAL, 6f) - .paddingDip(YogaEdge.VERTICAL, 2f) - .marginDip(YogaEdge.HORIZONTAL, 4f) - .backgroundRes(R.drawable.pending_note_background) - .textColor(hintColor)) - .child(Text.create(context) - .text(remoteUpdateTime) - .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) - .paddingDip(YogaEdge.HORIZONTAL, 6f) - .paddingDip(YogaEdge.VERTICAL, 2f) - .backgroundRes(R.drawable.pending_note_background) - .textColor(hintColor)) - ) - .child(SolidColor.create(context) - .color(hintColor) - .heightDip(0.5f) - .widthDip(196f) - .alignSelf(YogaAlign.CENTER) - .marginDip(YogaEdge.TOP, 12f) - .alpha(0.4f)) + .text(uuid) + .maxLines(1) + .ellipsize(TextUtils.TruncateAt.END) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .textColor(subtitleColor))) + .child( + Text.create(context) + .text(info) + .maxLines(5) + .paddingDip(YogaEdge.ALL, 8f) + .marginDip(YogaEdge.ALL, 8f) + .ellipsize(TextUtils.TruncateAt.MIDDLE) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_MONSERRAT) + .backgroundRes(R.drawable.pending_note_background) + .textColor(subtitleColor)) + .child( + Row.create(context) + .widthPercent(100f) + .paddingDip(YogaEdge.ALL, 4f) + .child( + Text.create(context) + .textRes(R.string.pending_backup_local_state) + .textSizeRes(R.dimen.font_size_small) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .textColor(hintColor)) + .child( + Text.create(context) + .textRes(localState) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + .child( + Text.create(context) + .text(localUpdateTime) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + ) + .child( + Row.create(context) + .widthPercent(100f) + .paddingDip(YogaEdge.ALL, 4f) + .child( + Text.create(context) + .textRes(R.string.pending_backup_remote_state) + .textSizeRes(R.dimen.font_size_small) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .textColor(hintColor)) + .child( + Text.create(context) + .textRes(remoteState) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .marginDip(YogaEdge.HORIZONTAL, 4f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + .child( + Text.create(context) + .text(remoteUpdateTime) + .textSizeRes(R.dimen.font_size_small) + .typeface(CoreConfig.FONT_OPEN_SANS) + .paddingDip(YogaEdge.HORIZONTAL, 6f) + .paddingDip(YogaEdge.VERTICAL, 2f) + .backgroundRes(R.drawable.pending_note_background) + .textColor(hintColor)) + ) + .child( + SolidColor.create(context) + .color(hintColor) + .heightDip(0.5f) + .widthDip(196f) + .alignSelf(YogaAlign.CENTER) + .marginDip(YogaEdge.TOP, 12f) + .alpha(0.4f)) return column.build() } @@ -250,17 +268,18 @@ class GDrivePendingBottomSheet : LithoBottomSheet() { override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { try { val component = Column.create(componentContext) - .widthPercent(100f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .paddingDip(YogaEdge.HORIZONTAL, 20f) - .child(getLithoBottomSheetTitle(componentContext) - .textRes(R.string.home_pending_backup_top_layout) - .marginDip(YogaEdge.HORIZONTAL, 0f)) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.home_pending_backup_top_layout) + .marginDip(YogaEdge.HORIZONTAL, 0f)) data.forEach { component.child( - PendingItemLayout.create(componentContext) - .option(it) - .onClick {}) + PendingItemLayout.create(componentContext) + .option(it) + .onClick {}) } return component.build() } finally { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt index 979ab87a..61e37f76 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteDatabase.kt @@ -19,46 +19,46 @@ import java.lang.ref.WeakReference class GDriveRemoteDatabase(weakContext: WeakReference) : RemoteController(weakContext) { override fun initSyncs() { notesSync = GDriveRemoteFolder( - dataType = RemoteDataType.NOTE, - database = remoteDatabase, - service = remoteService as GDriveServiceHelper, - onPendingChange = { verifyAndNotifyPendingStateChange() }, - serialiser = { it }, - uuidToObject = { - ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() - }) + dataType = RemoteDataType.NOTE, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, + serialiser = { it }, + uuidToObject = { + ApplicationBase.instance.notesDatabase().getByUUID(it)?.toExportedMarkdown() + }) notesMetaSync = GDriveRemoteFolder( - dataType = RemoteDataType.NOTE_META, - database = remoteDatabase, - service = remoteService as GDriveServiceHelper, - onPendingChange = { verifyAndNotifyPendingStateChange() }, - serialiser = { Gson().toJson(it) }, - uuidToObject = { - ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() - }) + dataType = RemoteDataType.NOTE_META, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, + serialiser = { Gson().toJson(it) }, + uuidToObject = { + ApplicationBase.instance.notesDatabase().getByUUID(it)?.getExportableNoteMeta() + }) tagsSync = GDriveRemoteFolder( - dataType = RemoteDataType.TAG, - database = remoteDatabase, - service = remoteService as GDriveServiceHelper, - onPendingChange = { verifyAndNotifyPendingStateChange() }, - serialiser = { Gson().toJson(it) }, - uuidToObject = { - ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() - }) + dataType = RemoteDataType.TAG, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, + serialiser = { Gson().toJson(it) }, + uuidToObject = { + ApplicationBase.instance.tagsDatabase().getByUUID(it)?.getExportableTag() + }) foldersSync = GDriveRemoteFolder( - dataType = RemoteDataType.FOLDER, - database = remoteDatabase, - service = remoteService as GDriveServiceHelper, - onPendingChange = { verifyAndNotifyPendingStateChange() }, - serialiser = { Gson().toJson(it) }, - uuidToObject = { - ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() - }) + dataType = RemoteDataType.FOLDER, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, + onPendingChange = { verifyAndNotifyPendingStateChange() }, + serialiser = { Gson().toJson(it) }, + uuidToObject = { + ApplicationBase.instance.foldersDatabase().getByUUID(it)?.getExportableFolder() + }) imageSync = GDriveRemoteImageFolder( - dataType = RemoteDataType.IMAGE, - database = remoteDatabase, - service = remoteService as GDriveServiceHelper, - onPendingChange = { verifyAndNotifyPendingStateChange() }) + dataType = RemoteDataType.IMAGE, + database = remoteDatabase, + service = remoteService as GDriveServiceHelper, + onPendingChange = { verifyAndNotifyPendingStateChange() }) } override fun getResourceIdForFolderName(folderName: String): String? { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt index c905d88b..49f066bc 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolder.kt @@ -10,12 +10,12 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean class GDriveRemoteFolder( - dataType: RemoteDataType, - database: RemoteUploadDataDao, - service: GDriveServiceHelper, - onPendingChange: () -> Unit, - val serialiser: (T) -> String, - val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, service, onPendingChange) { + dataType: RemoteDataType, + database: RemoteUploadDataDao, + service: GDriveServiceHelper, + onPendingChange: () -> Unit, + val serialiser: (T) -> String, + val uuidToObject: (String) -> T?) : GDriveRemoteFolderBase(dataType, database, service, onPendingChange) { private var networkOrAbsoluteFailure = AtomicBoolean(false) @@ -119,7 +119,7 @@ class GDriveRemoteFolder( when { localFileIds.containsKey(file.name) -> duplicateFilesToDelete.add(file.id) getTrueCurrentTime() - (file.modifiedTime?.value - ?: 0L) > TimeUnit.DAYS.toMillis(7) -> duplicateFilesToDelete.add(file.id) + ?: 0L) > TimeUnit.DAYS.toMillis(7) -> duplicateFilesToDelete.add(file.id) else -> { localFileIds[file.name] = file.id notifyDriveData(file, true) @@ -248,7 +248,7 @@ class GDriveRemoteFolder( } GlobalScope.launch { val timestamp = database.getByUUID(dataType.name, uuid)?.lastUpdateTimestamp - ?: getTrueCurrentTime() + ?: getTrueCurrentTime() service.createFileWithData(deletedFolderUid, uuid, uuid, timestamp) { file -> if (file !== null) { deletedFiles[uuid] = file.id diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt index 4dc7914e..23705efa 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteFolderBase.kt @@ -9,10 +9,10 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch abstract class GDriveRemoteFolderBase( - val dataType: RemoteDataType, - val database: RemoteUploadDataDao, - val service: GDriveServiceHelper, - val onPendingChange: () -> Unit): RemoteFolder { + val dataType: RemoteDataType, + val database: RemoteUploadDataDao, + val service: GDriveServiceHelper, + val onPendingChange: () -> Unit) : RemoteFolder { protected fun notifyDriveData(file: File, deleted: Boolean = false) { val modifiedTime = file.modifiedTime?.value ?: file.modifiedByMeTime?.value ?: 0L diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt index 10ea71c6..4b49a23a 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveRemoteImageFolder.kt @@ -12,8 +12,8 @@ import java.util.* import java.util.concurrent.atomic.AtomicBoolean data class ImageUUID( - val noteUuid: String, - val imageUuid: String) { + val noteUuid: String, + val imageUuid: String) { fun name(): String = "$noteUuid::$imageUuid" @@ -40,10 +40,10 @@ fun toImageUUID(imageUuid: String): ImageUUID? { } class GDriveRemoteImageFolder( - dataType: RemoteDataType, - database: RemoteUploadDataDao, - service: GDriveServiceHelper, - onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, service, onPendingChange) { + dataType: RemoteDataType, + database: RemoteUploadDataDao, + service: GDriveServiceHelper, + onPendingChange: () -> Unit) : GDriveRemoteFolderBase(dataType, database, service, onPendingChange) { private val networkOrAbsoluteFailure = AtomicBoolean(false) @@ -137,7 +137,7 @@ class GDriveRemoteImageFolder( val gDriveUUID = id.name() val timestamp = database.getByUUID(dataType.name, gDriveUUID)?.lastUpdateTimestamp - ?: getTrueCurrentTime() + ?: getTrueCurrentTime() val imageFile = sAppImageStorage.getFile(id.noteUuid, id.imageUuid) if (!imageFile.exists()) { // notifyDriveData(id, gDriveUUID, timestamp) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt index a9d22d5f..7ab89340 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveServiceHelper.kt @@ -118,9 +118,9 @@ class GDriveServiceHelper(private val mDriveService: Drive) : IRemoteService val files = result.result?.files ?: emptyList() val namesIdList = emptyList>().toMutableList() files.forEach { when { - (it.id === null || it.id.isBlank()) -> {} + (it.id === null || it.id.isBlank()) -> { + } else -> namesIdList.add(Pair(it.name, it.id)) } } @@ -178,10 +179,10 @@ class GDriveServiceHelper(private val mDriveService: Drive) : IRemoteService onSuccess(result.result) } @@ -292,10 +293,10 @@ class GDriveServiceHelper(private val mDriveService: Drive) : IRemoteService R.string.firebase_page_logging_in_button else -> R.string.firebase_page_login_button } return Column.create(context) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(VerticalScroll.create(context) - .flexGrow(1f) - .marginDip(YogaEdge.ALL, 8f) - .childComponent(FirebaseContentView.create(context))) - .child(Row.create(context) - .backgroundRes(R.drawable.login_button_active) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .marginDip(YogaEdge.ALL, 16f) - .child( - Image.create(context) - .drawableRes(R.drawable.ic_google_icon) - .heightDip(36f) - ) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColorRes(R.color.white) - .textRes(buttonTitle) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) - .clickHandler(FirebaseRootView.onGoogleClickEvent(context))) - .build() + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(FirebaseContentView.create(context))) + .child( + Row.create(context) + .backgroundRes(R.drawable.login_button_active) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.ic_google_icon) + .heightDip(36f) + ) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(FirebaseRootView.onGoogleClickEvent(context))) + .build() } @OnEvent(ClickEvent::class) @@ -66,53 +77,60 @@ object FirebaseContentViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { return Column.create(context) - .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_xxlarge) - .textRes(R.string.firebase_page_login_title) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .textRes(R.string.firebase_page_important_details) - .typeface(CoreConfig.FONT_MONSERRAT)) - .child(FirebaseIconView.create(context) - .marginDip(YogaEdge.TOP, 24f) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.icon_sync_disabled) - .titleRes(R.string.firebase_page_not_sync_details)) - .child(FirebaseIconView.create(context) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_delete_permanently) - .titleRes(R.string.firebase_page_remove_details)) - .build() + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.firebase_page_login_title) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.firebase_page_important_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child( + FirebaseIconView.create(context) + .marginDip(YogaEdge.TOP, 24f) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.icon_sync_disabled) + .titleRes(R.string.firebase_page_not_sync_details)) + .child( + FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_delete_permanently) + .titleRes(R.string.firebase_page_remove_details)) + .build() } } @LayoutSpec object FirebaseIconViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop(resType = ResType.COLOR) bgColor: Int, - @Prop(resType = ResType.DRAWABLE) icon: Drawable, - @Prop(resType = ResType.STRING) title: String): Component { + fun onCreate( + context: ComponentContext, + @Prop(resType = ResType.COLOR) bgColor: Int, + @Prop(resType = ResType.DRAWABLE) icon: Drawable, + @Prop(resType = ResType.STRING) title: String): Component { return Column.create(context) - .paddingDip(YogaEdge.HORIZONTAL, 32f) - .paddingDip(YogaEdge.VERTICAL, 24f) - .child(Image.create(context) - .drawable(icon.color(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) - .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) - .paddingDip(YogaEdge.ALL, 12f) - .marginDip(YogaEdge.BOTTOM, 12f) - .heightDip(64f)) - .child(Text.create(context) - .text(title) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .textSizeRes(R.dimen.font_size_normal) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT)) - .build() + .paddingDip(YogaEdge.HORIZONTAL, 32f) + .paddingDip(YogaEdge.VERTICAL, 24f) + .child( + Image.create(context) + .drawable(icon.color(sAppTheme.get(ThemeColorType.SECONDARY_TEXT))) + .background(LithoCircleDrawable(bgColor, Color.alpha(bgColor))) + .paddingDip(YogaEdge.ALL, 12f) + .marginDip(YogaEdge.BOTTOM, 12f) + .heightDip(64f)) + .child( + Text.create(context) + .text(title) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .textSizeRes(R.dimen.font_size_normal) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT)) + .build() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt index 688dcd08..ba1d4f48 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseLoginActivity.kt @@ -58,29 +58,29 @@ class FirebaseLoginActivity : ThemedActivity() { private fun setButton(state: Boolean) { loggingIn.set(state) component = FirebaseRootView.create(componentContext) - .onClick { - if (!hasAcceptedThePolicy()) { - IntentUtils.startActivity(this, DataPolicyActivity::class.java) - return@onClick - } - if (!loggingIn.get()) { - setButton(true) - sFirebaseKilled = false - signIn() - } + .onClick { + if (!hasAcceptedThePolicy()) { + IntentUtils.startActivity(this, DataPolicyActivity::class.java) + return@onClick + } + if (!loggingIn.get()) { + setButton(true) + sFirebaseKilled = false + signIn() } - .loggingIn(state) - .build() + } + .loggingIn(state) + .build() setContentView(LithoView.create(componentContext, component)) } private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder( - GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestScopes(Scope(DriveScopes.DRIVE_FILE)) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() googleSignInClient = GoogleSignIn.getClient(this, gso) } @@ -120,17 +120,17 @@ class FirebaseLoginActivity : ThemedActivity() { private fun firebaseAuthWithGoogle(account: GoogleSignInAccount) { val credential = GoogleAuthProvider.getCredential(account.idToken, null) firebaseAuth.signInWithCredential(credential) - .addOnCompleteListener(this, object : OnCompleteListener { - override fun onComplete(task: Task) { - if (task.isSuccessful()) { - val user = firebaseAuth.currentUser - onLoginSuccess(user) - } else { - Log.e("Firebase", "Failed") - onLoginFailure() - } + .addOnCompleteListener(this, object : OnCompleteListener { + override fun onComplete(task: Task) { + if (task.isSuccessful()) { + val user = firebaseAuth.currentUser + onLoginSuccess(user) + } else { + Log.e("Firebase", "Failed") + onLoginFailure() } - }) + } + }) } private fun onLoginSuccess(user: FirebaseUser?) { diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt index c92140de..ec93134b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivity.kt @@ -39,53 +39,53 @@ class FirebaseRemovalActivity : ThemedActivity(), GoogleApiClient.OnConnectionFa private fun setButton(state: Boolean) { loggingIn.set(state) component = FirebaseRemovalRootView.create(componentContext) - .onClick { - if (loggingIn.get()) { - return@onClick - } - - setButton(true) - forgettingInProcess = true - firebaseForgetMe( - onComplete = { - signOut { - forgettingInProcess = false - instance.authenticator().openLoginActivity(context)?.run() - finish() - } - }, - onFailure = { - forgettingInProcess = false - ToastHelper.show(context, "Failed logging you out. Try logging in again.") - context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) - finish() - }) + .onClick { + if (loggingIn.get()) { + return@onClick } - .removingItems(state) - .build() + + setButton(true) + forgettingInProcess = true + firebaseForgetMe( + onComplete = { + signOut { + forgettingInProcess = false + instance.authenticator().openLoginActivity(context)?.run() + finish() + } + }, + onFailure = { + forgettingInProcess = false + ToastHelper.show(context, "Failed logging you out. Try logging in again.") + context.startActivity(Intent(context, FirebaseLoginActivity::class.java)) + finish() + }) + } + .removingItems(state) + .build() setContentView(LithoView.create(componentContext, component)) } lateinit var mGoogleApiClient: GoogleApiClient private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder( - GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() mGoogleApiClient = GoogleApiClient - .Builder(this) - .enableAutoManage(this, this) - .addApi(Auth.GOOGLE_SIGN_IN_API, gso) - .build() + .Builder(this) + .enableAutoManage(this, this) + .addApi(Auth.GOOGLE_SIGN_IN_API, gso) + .build() } private fun signOut(onSuccess: () -> Unit) { if (mGoogleApiClient.isConnecting) { Handler().postDelayed({ - signOut(onSuccess) - }, 500) + signOut(onSuccess) + }, 500) return } @@ -103,7 +103,6 @@ class FirebaseRemovalActivity : ThemedActivity(), GoogleApiClient.OnConnectionFa ToastHelper.show(context, "Failed logging you out properly. Try again later.") } - override fun onBackPressed() { super.onBackPressed() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt index 7ad1551a..13c9062b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt @@ -1,7 +1,11 @@ package com.bijoysingh.quicknote.firebase.activity import android.text.Layout -import com.facebook.litho.* +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row import com.facebook.litho.annotations.LayoutSpec import com.facebook.litho.annotations.OnCreateLayout import com.facebook.litho.annotations.OnEvent @@ -12,7 +16,6 @@ import com.facebook.litho.widget.VerticalScroll import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -20,38 +23,42 @@ import com.maubis.scarlet.base.support.ui.ThemeColorType @LayoutSpec object FirebaseRemovalRootViewSpec { @OnCreateLayout - fun onCreate(context: ComponentContext, - @Prop removingItems: Boolean): Component { + fun onCreate( + context: ComponentContext, + @Prop removingItems: Boolean): Component { val buttonTitle = when { removingItems -> R.string.firebase_removal_page_clearing_button else -> R.string.firebase_removal_page_clear_button } return Column.create(context) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(VerticalScroll.create(context) - .flexGrow(1f) - .marginDip(YogaEdge.ALL, 8f) - .childComponent(FirebaseRemovalContentView.create(context))) - .child(Row.create(context) - .backgroundRes(R.drawable.login_button_disabled) - .alignItems(YogaAlign.CENTER) - .paddingDip(YogaEdge.HORIZONTAL, 12f) - .paddingDip(YogaEdge.VERTICAL, 8f) - .marginDip(YogaEdge.ALL, 16f) - .child( - Image.create(context) - .drawableRes(R.drawable.ic_google_icon) - .heightDip(36f) - ) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColorRes(R.color.white) - .textRes(buttonTitle) - .textAlignment(Layout.Alignment.ALIGN_CENTER) - .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) - .clickHandler(FirebaseRemovalRootView.onLogoutClickEvent(context))) - .build() + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + VerticalScroll.create(context) + .flexGrow(1f) + .marginDip(YogaEdge.ALL, 8f) + .childComponent(FirebaseRemovalContentView.create(context))) + .child( + Row.create(context) + .backgroundRes(R.drawable.login_button_disabled) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.HORIZONTAL, 12f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .marginDip(YogaEdge.ALL, 16f) + .child( + Image.create(context) + .drawableRes(R.drawable.ic_google_icon) + .heightDip(36f) + ) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColorRes(R.color.white) + .textRes(buttonTitle) + .textAlignment(Layout.Alignment.ALIGN_CENTER) + .flexGrow(1f) + .typeface(CoreConfig.FONT_MONSERRAT)) + .clickHandler(FirebaseRemovalRootView.onLogoutClickEvent(context))) + .build() } @OnEvent(ClickEvent::class) @@ -65,58 +72,65 @@ object FirebaseRemovalContentViewSpec { @OnCreateLayout fun onCreate(context: ComponentContext): Component { return Column.create(context) - .paddingDip(YogaEdge.ALL, 16f) - .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_xxlarge) - .textRes(R.string.firebase_removal_page_login_title) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) - .child(Text.create(context) - .textSizeRes(R.dimen.font_size_large) - .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .textRes(R.string.firebase_removal_page_important_details) - .typeface(CoreConfig.FONT_MONSERRAT)) - .child(Row.create(context) - .marginDip(YogaEdge.TOP, 24f) - .child( - FirebaseIconView.create(context) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_privacy_policy) - .titleRes(R.string.firebase_removal_page_more_privacy_details) - .flexGrow(1f)) - .child(FirebaseIconView.create(context) - .bgColorRes(R.color.dark_low_hint_text) - .iconRes(R.drawable.ic_image_gallery) - .titleRes(R.string.firebase_removal_page_photo_upload_details) - .flexGrow(1f)) - ) - .child(Text.create(context) - .marginDip(YogaEdge.HORIZONTAL, 16f) - .paddingDip(YogaEdge.BOTTOM, 10f) - .paddingDip(YogaEdge.TOP, 20f) - .textSizeRes(R.dimen.font_size_normal) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .textRes(R.string.firebase_removal_page_whats_next_details) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) - .child(Text.create(context) - .backgroundRes(R.drawable.secondary_rounded_bg) - .marginDip(YogaEdge.VERTICAL, 4f) - .marginDip(YogaEdge.HORIZONTAL, 16f) - .paddingDip(YogaEdge.ALL, 12f) - .textSizeRes(R.dimen.font_size_normal) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .textRes(R.string.firebase_removal_page_remove_details) - .typeface(CoreConfig.FONT_MONSERRAT)) - .child(Text.create(context) - .backgroundRes(R.drawable.secondary_rounded_bg) - .marginDip(YogaEdge.VERTICAL, 4f) - .marginDip(YogaEdge.HORIZONTAL, 16f) - .paddingDip(YogaEdge.ALL, 12f) - .textSizeRes(R.dimen.font_size_normal) - .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .textRes(R.string.firebase_removal_page_next_details) - .typeface(CoreConfig.FONT_MONSERRAT)) - .build() + .paddingDip(YogaEdge.ALL, 16f) + .backgroundColor(sAppTheme.get(ThemeColorType.BACKGROUND)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_xxlarge) + .textRes(R.string.firebase_removal_page_login_title) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child( + Text.create(context) + .textSizeRes(R.dimen.font_size_large) + .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) + .textRes(R.string.firebase_removal_page_important_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child( + Row.create(context) + .marginDip(YogaEdge.TOP, 24f) + .child( + FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_privacy_policy) + .titleRes(R.string.firebase_removal_page_more_privacy_details) + .flexGrow(1f)) + .child( + FirebaseIconView.create(context) + .bgColorRes(R.color.dark_low_hint_text) + .iconRes(R.drawable.ic_image_gallery) + .titleRes(R.string.firebase_removal_page_photo_upload_details) + .flexGrow(1f)) + ) + .child( + Text.create(context) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.BOTTOM, 10f) + .paddingDip(YogaEdge.TOP, 20f) + .textSizeRes(R.dimen.font_size_normal) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.firebase_removal_page_whats_next_details) + .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .child( + Text.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .marginDip(YogaEdge.VERTICAL, 4f) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.ALL, 12f) + .textSizeRes(R.dimen.font_size_normal) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.firebase_removal_page_remove_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .child( + Text.create(context) + .backgroundRes(R.drawable.secondary_rounded_bg) + .marginDip(YogaEdge.VERTICAL, 4f) + .marginDip(YogaEdge.HORIZONTAL, 16f) + .paddingDip(YogaEdge.ALL, 12f) + .textSizeRes(R.dimen.font_size_normal) + .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) + .textRes(R.string.firebase_removal_page_next_details) + .typeface(CoreConfig.FONT_MONSERRAT)) + .build() } } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt index f71293db..adeca272 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/ForgetMeActivity.kt @@ -61,13 +61,13 @@ class ForgetMeActivity : ThemedActivity() { forgettingInProcess = true firebaseForgetMe( - onComplete = { - sFirebaseKilled = true - finish() - }, - onFailure = { - reauthAndDelete() - }) + onComplete = { + sFirebaseKilled = true + finish() + }, + onFailure = { + reauthAndDelete() + }) } cancelBtn.setOnClickListener { @@ -86,30 +86,29 @@ class ForgetMeActivity : ThemedActivity() { firebase?.deleteEverything() FirebaseAuth.getInstance().currentUser - ?.delete() - ?.addOnCompleteListener { - if (it.isSuccessful) { - firebase?.logout() - gDrive?.logout() - sFirebaseKilled = true - sGDriveLoggedIn = false - onComplete() - return@addOnCompleteListener - } - onFailure() + ?.delete() + ?.addOnCompleteListener { + if (it.isSuccessful) { + firebase?.logout() + gDrive?.logout() + sFirebaseKilled = true + sGDriveLoggedIn = false + onComplete() + return@addOnCompleteListener } + onFailure() + } } } - /** * Google Login for reauth */ private fun setupGoogleLogin() { val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() googleSignInClient = GoogleSignIn.getClient(this, gso); } @@ -145,26 +144,26 @@ class ForgetMeActivity : ThemedActivity() { val user = FirebaseAuth.getInstance().currentUser user?.reauthenticate(credential) - ?.addOnCompleteListener { - if (!it.isSuccessful) { - ToastHelper.show(this, "Reauthentication of account failed.") - return@addOnCompleteListener - } - deleteUser(user) + ?.addOnCompleteListener { + if (!it.isSuccessful) { + ToastHelper.show(this, "Reauthentication of account failed.") + return@addOnCompleteListener } + deleteUser(user) + } } protected fun deleteUser(user: FirebaseUser) { user.delete() - .addOnCompleteListener { - if (!it.isSuccessful) { - ToastHelper.show(this, "Deletion of account failed") - return@addOnCompleteListener - } - - ApplicationBase.instance.authenticator().logout() - finish() + .addOnCompleteListener { + if (!it.isSuccessful) { + ToastHelper.show(this, "Deletion of account failed") + return@addOnCompleteListener } + + ApplicationBase.instance.authenticator().logout() + finish() + } } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt index 1e68b10d..1f19abaa 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseFolder.kt @@ -6,11 +6,11 @@ import java.util.* // TODO: Remove this on Firebase deprecation class FirebaseFolder( - val uuid: String, - val title: String, - val timestamp: Long, - val updateTimestamp: Long, - val color: Int) : IFolderContainer { + val uuid: String, + val title: String, + val timestamp: Long, + val updateTimestamp: Long, + val color: Int) : IFolderContainer { @Exclude override fun uuid(): String = uuid @@ -28,9 +28,9 @@ class FirebaseFolder( override fun color(): Int = color constructor() : this( - "invalid", - "", - Calendar.getInstance().timeInMillis, - Calendar.getInstance().timeInMillis, - -0xff8695) + "invalid", + "", + Calendar.getInstance().timeInMillis, + Calendar.getInstance().timeInMillis, + -0xff8695) } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt index b61dcdd8..ae525a0e 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseNote.kt @@ -7,16 +7,16 @@ import java.util.* // TODO: Remove this on Firebase deprecation class FirebaseNote( - val uuid: String, - val description: String, - val timestamp: Long, - val updateTimestamp: Long, - val color: Int, - val state: String, - val tags: String, - val locked: Boolean, - val pinned: Boolean, - val folder: String) : INoteContainer { + val uuid: String, + val description: String, + val timestamp: Long, + val updateTimestamp: Long, + val color: Int, + val state: String, + val tags: String, + val locked: Boolean, + val pinned: Boolean, + val folder: String) : INoteContainer { @Exclude override fun uuid(): String = uuid @@ -52,14 +52,14 @@ class FirebaseNote( override fun folder(): String = folder constructor() : this( - "invalid", - "", - Calendar.getInstance().timeInMillis, - Calendar.getInstance().timeInMillis, - -0xff8695, - NoteState.DEFAULT.name, - "", - false, - false, - "") + "invalid", + "", + Calendar.getInstance().timeInMillis, + Calendar.getInstance().timeInMillis, + -0xff8695, + NoteState.DEFAULT.name, + "", + false, + false, + "") } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt index dae97e72..c33acf6b 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FirebaseTag.kt @@ -5,8 +5,8 @@ import com.maubis.scarlet.base.core.tag.ITagContainer // TODO: Remove this on Firebase deprecation class FirebaseTag( - val uuid: String, - val title: String) : ITagContainer { + val uuid: String, + val title: String) : ITagContainer { @Exclude override fun title(): String = title diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt index 84c7849d..1a5b1dbe 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/FolderExtensions.kt @@ -5,10 +5,10 @@ import com.maubis.scarlet.base.database.room.folder.Folder // TODO: Remove this on Firebase deprecation fun Folder.getFirebaseFolder(): FirebaseFolder { return FirebaseFolder( - uuid, - title, - timestamp, - updateTimestamp, - color + uuid, + title, + timestamp, + updateTimestamp, + color ) } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt index 78132758..f6fa41ed 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/data/NoteExtensions.kt @@ -5,15 +5,15 @@ import com.maubis.scarlet.base.database.room.note.Note // TODO: Remove this on Firebase deprecation fun Note.getFirebaseNote(): FirebaseNote { return FirebaseNote( - uuid, - description, - timestamp, - updateTimestamp, - color, - state, - if (tags == null) "" else tags, - locked, - pinned, - folder + uuid, + description, + timestamp, + updateTimestamp, + color, + state, + if (tags == null) "" else tags, + locked, + pinned, + folder ) } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt index 68e49d07..0f5d5a38 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseFolderReference.kt @@ -9,16 +9,15 @@ import com.google.firebase.database.DatabaseError import com.google.firebase.database.FirebaseDatabase import com.maubis.scarlet.base.support.utils.maybeThrow - /** * Functions for Database Reference for Firebase Notes */ fun FirebaseRemoteDatabase.initFolderReference(userId: String) { firebaseFolder = FirebaseDatabase - .getInstance() - .getReference() - .child("folders") - .child(userId) + .getInstance() + .getReference() + .child("folders") + .child(userId) setFolderListener() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt index 58f97998..4d7aad77 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseNoteReference.kt @@ -14,10 +14,10 @@ import com.maubis.scarlet.base.support.utils.maybeThrow */ fun FirebaseRemoteDatabase.initNoteReference(userId: String) { firebaseNote = FirebaseDatabase - .getInstance() - .getReference() - .child("notes") - .child(userId) + .getInstance() + .getReference() + .child("notes") + .child(userId) setNoteListener() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt index 7dd44e3f..5865dc2c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/FirebaseTagReference.kt @@ -9,17 +9,16 @@ import com.google.firebase.database.DatabaseError import com.google.firebase.database.FirebaseDatabase import com.maubis.scarlet.base.support.utils.maybeThrow - /** * Functions for Database Reference for Firebase Notes */ fun FirebaseRemoteDatabase.initTagReference(userId: String) { firebaseTag = FirebaseDatabase - .getInstance() - .getReference() - .child("tags") - .child(userId) + .getInstance() + .getReference() + .child("tags") + .child(userId) setTagListener() } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt index a98aedcb..d049213c 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt @@ -40,14 +40,14 @@ class RemoteConfigFetcher() : IRemoteConfigFetcher { fun fetchConfig(context: Context) { val request = object : StringRequest( - Request.Method.GET, - REMOTE_CONFIG_URL, - Response.Listener { response -> onSuccess(response) }, - Response.ErrorListener { _ -> }) {} + Request.Method.GET, + REMOTE_CONFIG_URL, + Response.Listener { response -> onSuccess(response) }, + Response.ErrorListener { _ -> }) {} request.retryPolicy = DefaultRetryPolicy( - DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, - DefaultRetryPolicy.DEFAULT_MAX_RETRIES, - DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) + DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, + DefaultRetryPolicy.DEFAULT_MAX_RETRIES, + DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) request.setShouldCache(false) Volley.newRequestQueue(context).add(request) } From bfcf0962f62b8a2e2d22a4f82111261dc9e76df9 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 31 Oct 2019 00:25:05 +0000 Subject: [PATCH 112/134] Refactoring Flavor --- .../java/com/bijoysingh/quicknote/MaterialNotes.kt | 3 +++ .../com/maubis/scarlet/base/config/ApplicationBase.kt | 3 +++ .../java/com/maubis/scarlet/base/config/CoreConfig.kt | 2 -- .../maubis/scarlet/base/config/MaterialNoteConfig.kt | 2 -- .../base/export/sheet/ExportNotesBottomSheet.kt | 4 ++-- .../maubis/scarlet/base/support/utils/FlavorUtils.kt | 10 ++++++---- .../src/main/java/com/bijoysingh/quicknote/Scarlet.kt | 6 ++++++ .../quicknote/firebase/support/RemoteConfigFetcher.kt | 4 ++-- .../com/bijoysingh/quicknote/scarlet/ScarletConfig.kt | 6 ------ 9 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt b/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt index e3f22afb..0da8f077 100644 --- a/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt +++ b/app/src/main/java/com/bijoysingh/quicknote/MaterialNotes.kt @@ -3,11 +3,14 @@ package com.bijoysingh.quicknote import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.MaterialNoteConfig import com.maubis.scarlet.base.export.support.ExternalFolderSync +import com.maubis.scarlet.base.support.utils.Flavor class MaterialNotes : ApplicationBase() { override fun onCreate() { super.onCreate() + sAppFlavor = Flavor.NONE + ApplicationBase.instance = MaterialNoteConfig(this) ExternalFolderSync.setup(this) } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index 75043d56..adfa35f3 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -10,6 +10,7 @@ import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator import com.maubis.scarlet.base.support.ui.ThemeManager import com.maubis.scarlet.base.support.utils.DateFormatUtils +import com.maubis.scarlet.base.support.utils.Flavor import com.maubis.scarlet.base.support.utils.ImageCache import com.maubis.scarlet.base.support.utils.maybeThrow import com.maubis.scarlet.base.support.utils.sDateFormat @@ -41,6 +42,8 @@ abstract class ApplicationBase : Application() { companion object { lateinit var instance: CoreConfig + lateinit var sAppFlavor: Flavor + lateinit var sAppImageStorage: NoteImage lateinit var sAppImageCache: ImageCache diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 39e06bf8..992a9374 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -59,8 +59,6 @@ abstract class CoreConfig(context: Context) { abstract fun startListener(activity: AppCompatActivity) - abstract fun appFlavor(): Flavor - companion object { val notesDb get() = instance.notesDatabase() val tagsDb get() = instance.tagsDatabase() diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index 43ed8190..ddaf7afc 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -55,6 +55,4 @@ open class MaterialNoteConfig(context: Context) : CoreConfig(context) { } override fun startListener(activity: AppCompatActivity) {} - - override fun appFlavor(): Flavor = Flavor.NONE } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt index 06598578..64896793 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExportNotesBottomSheet.kt @@ -12,7 +12,7 @@ import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.ToastHelper import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppFlavor import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.support.GenericFileProvider import com.maubis.scarlet.base.export.support.NoteExporter @@ -35,7 +35,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch val NOTES_EXPORT_FOLDER - get() = when (ApplicationBase.instance.appFlavor()) { + get() = when (sAppFlavor) { Flavor.NONE -> "MaterialNotes" Flavor.LITE -> "Scarlet" Flavor.PRO -> "ScarletPro" diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt index 21bed475..adb24657 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt @@ -1,7 +1,9 @@ package com.maubis.scarlet.base.support.utils import android.content.Context +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppFlavor import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -30,8 +32,8 @@ object FlavorUtils { return sAppPreferences.get(KEY_PRO_APP_INSTALLED, false) } - fun isPro() = instance.appFlavor() == Flavor.PRO - fun isLite() = instance.appFlavor() == Flavor.LITE - fun isPlayStore() = instance.appFlavor() != Flavor.NONE - fun isOpenSource() = instance.appFlavor() == Flavor.NONE + fun isPro() = sAppFlavor == Flavor.PRO + fun isLite() = sAppFlavor == Flavor.LITE + fun isPlayStore() = sAppFlavor != Flavor.NONE + fun isOpenSource() = sAppFlavor == Flavor.NONE } \ No newline at end of file diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt index a5a65519..91e84a29 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/Scarlet.kt @@ -7,11 +7,17 @@ import com.bijoysingh.quicknote.scarlet.ScarletConfig import com.github.bijoysingh.starter.prefs.Store import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.export.support.ExternalFolderSync +import com.maubis.scarlet.base.support.utils.Flavor class Scarlet : ApplicationBase() { override fun onCreate() { super.onCreate() + sAppFlavor = when (BuildConfig.FLAVOR) { + "lite" -> Flavor.LITE + "full" -> Flavor.PRO + else -> Flavor.NONE + } remoteConfig = Store.get(this, "gdrive_config") diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt index d049213c..b0beffe9 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/support/RemoteConfigFetcher.kt @@ -8,7 +8,7 @@ import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley import com.bijoysingh.quicknote.BuildConfig import com.google.gson.Gson -import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppFlavor import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.config.remote.RemoteConfig @@ -30,7 +30,7 @@ class RemoteConfigFetcher() : IRemoteConfigFetcher { } override fun isLatestVersion(): Boolean { - val latestVersion = when (ApplicationBase.instance.appFlavor()) { + val latestVersion = when (sAppFlavor) { Flavor.PRO -> sAppPreferences.get(KEY_RC_FULL_VERSION, 0) Flavor.LITE -> sAppPreferences.get(KEY_RC_LITE_VERSION, 0) else -> 0 diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt index 8915471e..716a6ceb 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/scarlet/ScarletConfig.kt @@ -32,12 +32,6 @@ class ScarletConfig(context: Context) : MaterialNoteConfig(context) { override fun remoteDatabaseState(): IRemoteDatabaseState = remoteDatabaseStateController!! - override fun appFlavor(): Flavor = when (BuildConfig.FLAVOR) { - "lite" -> Flavor.LITE - "full" -> Flavor.PRO - else -> Flavor.NONE - } - override fun startListener(activity: AppCompatActivity) { openIfNeeded(activity) } From 7451bf777fbff6879d96bd0fd5eb43a495e74ef3 Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Thu, 31 Oct 2019 00:46:47 +0000 Subject: [PATCH 113/134] [Typeface][1] Adding initial abstraction for more typefaces --- .../scarlet/base/config/ApplicationBase.kt | 3 +++ .../maubis/scarlet/base/config/CoreConfig.kt | 12 ----------- .../scarlet/base/config/MaterialNoteConfig.kt | 1 - .../sheet/ExternalFolderSyncBottomSheet.kt | 4 ++-- .../main/sheets/HomeOptionsBottomSheet.kt | 5 +++-- .../sheets/InstallProUpsellBottomSheet.kt | 4 ++-- .../base/main/sheets/WhatsNewBottomSheet.kt | 4 ++-- .../main/specs/MainActivityBottomBarSpec.kt | 11 +++++----- .../folder/SelectorFolderRecyclerHolder.kt | 4 ++-- .../sheet/FolderChooserBottomSheetBase.kt | 5 +++-- .../security/activity/AppLockActivitySpecs.kt | 10 ++++----- .../security/sheets/PincodeBottomSheet.kt | 8 +++---- .../base/settings/sheet/AboutUsBottomSheet.kt | 6 +++--- .../settings/sheet/OpenSourceBottomSheet.kt | 4 ++-- .../base/support/sheets/LithoBottomSheet.kt | 7 +++---- .../sheets/LithoChooseOptionBottomSheet.kt | 4 ++-- .../support/sheets/LithoOptionBottomSheet.kt | 9 ++++---- .../base/support/specs/BottomSheetBarSpec.kt | 6 +++--- .../base/support/specs/CounterChooserSpec.kt | 4 ++-- .../base/support/specs/GridSectionViewSpec.kt | 6 +++--- .../support/ui/font/TypefaceController.kt | 21 +++++++++++++++++++ .../scarlet/base/support/utils/FlavorUtils.kt | 2 -- .../quicknote/drive/GDriveActivitySpecs.kt | 11 +++++----- .../drive/GDriveLogoutActivitySpecs.kt | 7 ++++--- .../drive/GDrivePendingBottomSheet.kt | 19 +++++++++-------- .../activity/FirebaseActivitySpecs.kt | 9 ++++---- .../activity/FirebaseRemovalActivitySpecs.kt | 13 ++++++------ 27 files changed, 106 insertions(+), 93 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt diff --git a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt index adfa35f3..a5591cd6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/ApplicationBase.kt @@ -9,6 +9,7 @@ import com.maubis.scarlet.base.core.note.NoteImage import com.maubis.scarlet.base.export.remote.FolderRemoteDatabase import com.maubis.scarlet.base.note.reminders.ReminderJobCreator import com.maubis.scarlet.base.support.ui.ThemeManager +import com.maubis.scarlet.base.support.ui.font.TypefaceController import com.maubis.scarlet.base.support.utils.DateFormatUtils import com.maubis.scarlet.base.support.utils.Flavor import com.maubis.scarlet.base.support.utils.ImageCache @@ -37,6 +38,7 @@ abstract class ApplicationBase : Application() { // Setup Application Theme sAppTheme = ThemeManager() sAppTheme.setup(this) + sAppTypeface = TypefaceController(this) } companion object { @@ -50,6 +52,7 @@ abstract class ApplicationBase : Application() { lateinit var sAppPreferences: Store lateinit var sAppTheme: ThemeManager + lateinit var sAppTypeface: TypefaceController var folderSync: FolderRemoteDatabase? = null } diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 992a9374..2acaad83 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -21,7 +21,6 @@ import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.support.utils.Flavor abstract class CoreConfig(context: Context) { @@ -29,12 +28,6 @@ abstract class CoreConfig(context: Context) { Reprint.initialize(context) config.spanConfig.headingTypeface = ResourcesCompat.getFont(context, R.font.monserrat) ?: Typeface.DEFAULT - FONT_MONSERRAT = config.spanConfig.headingTypeface - FONT_MONSERRAT_MEDIUM = ResourcesCompat.getFont(context, R.font.monserrat_medium) - ?: Typeface.DEFAULT - FONT_MONSERRAT_BOLD = ResourcesCompat.getFont(context, R.font.monserrat_bold) - ?: Typeface.DEFAULT - FONT_OPEN_SANS = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT } abstract fun database(): AppDatabase @@ -63,10 +56,5 @@ abstract class CoreConfig(context: Context) { val notesDb get() = instance.notesDatabase() val tagsDb get() = instance.tagsDatabase() val foldersDb get() = instance.foldersDatabase() - - var FONT_MONSERRAT: Typeface = Typeface.DEFAULT - var FONT_MONSERRAT_MEDIUM: Typeface = Typeface.DEFAULT - var FONT_MONSERRAT_BOLD: Typeface = Typeface.DEFAULT - var FONT_OPEN_SANS: Typeface = Typeface.DEFAULT } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt index ddaf7afc..da4c7767 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/MaterialNoteConfig.kt @@ -20,7 +20,6 @@ import com.maubis.scarlet.base.database.room.AppDatabase import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.database.room.tag.Tag -import com.maubis.scarlet.base.support.utils.Flavor open class MaterialNoteConfig(context: Context) : CoreConfig(context) { val db = AppDatabase.createDatabase(context) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index c3abaf93..e8b6007d 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -9,7 +9,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.export.support.ExternalFolderSync import com.maubis.scarlet.base.export.support.sExternalFolderSync import com.maubis.scarlet.base.export.support.sFolderSyncBackupLocked @@ -43,7 +43,7 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .child( Text.create(componentContext) .textSizeRes(R.dimen.font_size_xlarge) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textRes(R.string.import_export_layout_folder_sync_folder) .paddingDip(YogaEdge.HORIZONTAL, 20f) .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt index 98d1e586..16fdd1d1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/HomeOptionsBottomSheet.kt @@ -17,6 +17,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.tag.TagBuilder import com.maubis.scarlet.base.database.room.tag.Tag @@ -63,14 +64,14 @@ object TagItemLayoutSpec { bgColor = selectedColor bgAlpha = 200 textColor = selectedColor - typeface = CoreConfig.FONT_MONSERRAT_MEDIUM + typeface = sAppTypeface.subHeading() } false -> { icon = R.drawable.ic_action_label_unselected bgColor = titleColor bgAlpha = 15 textColor = titleColor - typeface = CoreConfig.FONT_MONSERRAT + typeface = sAppTypeface.title() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt index ebec9dbf..bf13d781 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/InstallProUpsellBottomSheet.kt @@ -8,8 +8,8 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -42,7 +42,7 @@ class InstallProUpsellBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.why_install_pro) - .typeface(FONT_MONSERRAT) + .typeface(ApplicationBase.sAppTypeface.title()) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child( GridSectionView.create(componentContext) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt index 17aeae03..de293910 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/WhatsNewBottomSheet.kt @@ -8,7 +8,7 @@ import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -47,7 +47,7 @@ class WhatsNewBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.whats_new_sheet_subtitle) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child( GridSectionView.create(componentContext) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt index 26f7e7cb..11a9b38b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/specs/MainActivityBottomBarSpec.kt @@ -24,8 +24,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT_MEDIUM +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder import com.maubis.scarlet.base.main.sheets.HomeOptionsBottomSheet @@ -136,7 +135,7 @@ object MainActivityFolderBottomBarSpec { }) row.child( Text.create(context) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textAlignment(Layout.Alignment.ALIGN_CENTER) .flexGrow(1f) .text(folder.title) @@ -187,13 +186,13 @@ object MainActivityDisabledSyncSpec { .paddingDip(YogaEdge.ALL, 8f) .child( Text.create(context) - .typeface(FONT_MONSERRAT_MEDIUM) + .typeface(sAppTypeface.subHeading()) .textRes(R.string.firebase_no_sync_warning) .textSizeRes(R.dimen.font_size_normal) .textColor(colorConfig.toolbarIconColor)) .child( Text.create(context) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textRes(R.string.firebase_no_sync_warning_details) .textSizeRes(R.dimen.font_size_small) .textColor(colorConfig.toolbarIconColor))) @@ -252,7 +251,7 @@ object MainActivitySyncingNowSpec { .child(syncIcon) .child( Text.create(context) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textRes(syncText) .textSizeRes(R.dimen.font_size_normal) .textColorRes(R.color.light_secondary_text))) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt index 4a244b2b..65e4b2a9 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/SelectorFolderRecyclerHolder.kt @@ -7,7 +7,7 @@ import android.widget.ImageView import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.R -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.main.recycler.setFullSpan import com.maubis.scarlet.base.support.recycler.RecyclerItem import com.maubis.scarlet.base.support.ui.CircleDrawable @@ -28,7 +28,7 @@ class SelectorFolderRecyclerHolder(context: Context, view: View) : RecyclerViewH val item = itemData as SelectorFolderRecyclerItem title.text = item.title title.setTextColor(item.titleColor) - title.typeface = FONT_MONSERRAT + title.typeface = sAppTypeface.title() title.alpha = 0.8f icon.setColorFilter(item.iconColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt index f9a62cfc..28ec7afe 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/sheet/FolderChooserBottomSheetBase.kt @@ -18,6 +18,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.core.folder.FolderBuilder import com.maubis.scarlet.base.database.room.folder.Folder @@ -55,14 +56,14 @@ object FolderItemLayoutSpec { bgColor = selectedColor bgAlpha = 200 textColor = selectedColor - typeface = CoreConfig.FONT_MONSERRAT_MEDIUM + typeface = sAppTypeface.subHeading() } false -> { icon = R.drawable.ic_folder bgColor = titleColor bgAlpha = 15 textColor = titleColor - typeface = CoreConfig.FONT_MONSERRAT + typeface = sAppTypeface.title() } } diff --git a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt index 98f7747c..a3df547e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/activity/AppLockActivitySpecs.kt @@ -21,7 +21,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.specs.EmptySpec import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.utils.getEditorActionListener @@ -67,7 +67,7 @@ object AppLockViewSpec { .textAlignment(Layout.Alignment.ALIGN_CENTER) .paddingDip(YogaEdge.VERTICAL, 12f) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .clickHandler(AppLockView.onUnlockClick(context)))) .build() } @@ -103,13 +103,13 @@ object AppLockContentViewSpec { .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.app_lock_title) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .typeface(sAppTypeface.heading())) .child( Text.create(context) .textSizeRes(R.dimen.font_size_large) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(description) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .child(EmptySpec.create(context).flexGrow(1f)) .child( EditText.create(context) @@ -121,7 +121,7 @@ object AppLockContentViewSpec { .alignSelf(YogaAlign.CENTER) .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index 1ce29ec9..c6a66db4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -26,7 +26,7 @@ import com.github.ajalt.reprint.core.Reprint import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.security.controller.PinLockController import com.maubis.scarlet.base.security.controller.PinLockController.isPinCodeEnabled import com.maubis.scarlet.base.security.controller.PinLockController.needsLockCheck @@ -98,7 +98,7 @@ object PincodeSheetViewSpec { .hint("****") .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD) .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) .paddingDip(YogaEdge.HORIZONTAL, 22f) .paddingDip(YogaEdge.VERTICAL, 6f) @@ -132,7 +132,7 @@ object PincodeSheetViewSpec { .textAlignment(Layout.Alignment.ALIGN_CENTER) .paddingDip(YogaEdge.VERTICAL, 12f) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .clickHandler(PincodeSheetView.onRemoveClick(context)) else -> null } @@ -147,7 +147,7 @@ object PincodeSheetViewSpec { .textAlignment(Layout.Alignment.ALIGN_CENTER) .paddingDip(YogaEdge.VERTICAL, 12f) .paddingDip(YogaEdge.HORIZONTAL, 20f) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .clickHandler(PincodeSheetView.onActionClick(context)))) return component.build() } diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index e6c4ac2e..9be181ff 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -10,7 +10,7 @@ import com.github.bijoysingh.starter.util.IntentUtils import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -53,7 +53,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_about_app) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child( Text.create(componentContext) @@ -66,7 +66,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.about_page_app_version) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child( Text.create(componentContext) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 73629833..9db8f77a 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -12,7 +12,7 @@ import com.maubis.markdown.Markdown import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -45,7 +45,7 @@ class OpenSourceBottomSheet : LithoBottomSheet() { .textSizeRes(R.dimen.font_size_xlarge) .marginDip(YogaEdge.BOTTOM, 4f) .textRes(R.string.osp_page_libraries) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child( Text.create(componentContext) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt index e6195c90..8b22a4c1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoBottomSheet.kt @@ -19,8 +19,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.ui.BottomSheetTabletDialog import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -31,7 +30,7 @@ fun openSheet(activity: AppCompatActivity, sheet: LithoBottomSheet) { fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { return Text.create(context) .textSizeRes(R.dimen.font_size_xxxlarge) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD) + .typeface(sAppTypeface.heading()) .marginDip(YogaEdge.HORIZONTAL, 20f) .marginDip(YogaEdge.TOP, 18f) .marginDip(YogaEdge.BOTTOM, 8f) @@ -41,7 +40,7 @@ fun getLithoBottomSheetTitle(context: ComponentContext): Text.Builder { fun getLithoBottomSheetButton(context: ComponentContext): Text.Builder { return Text.create(context) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 12f) .paddingDip(YogaEdge.HORIZONTAL, 24f) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt index 93e129af..e712f66e 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoChooseOptionBottomSheet.kt @@ -17,7 +17,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -44,7 +44,7 @@ object ChooseOptionItemLayoutSpec { Text.create(context) .textRes(option.title) .textSizeRes(R.dimen.font_size_normal) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textStyle(Typeface.BOLD) .textColor(titleColor) .flexGrow(1f)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt index abdc7a8f..d089a4ae 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/LithoOptionBottomSheet.kt @@ -17,8 +17,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_MONSERRAT -import com.maubis.scarlet.base.config.CoreConfig.Companion.FONT_OPEN_SANS +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.specs.RoundIcon import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -71,14 +70,14 @@ object OptionItemLayoutSpec { Text.create(context) .textRes(option.title) .textSizeRes(R.dimen.font_size_normal) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textStyle(BOLD) .textColor(titleColor)) .child( Text.create(context) .text(subtitle) .textSizeRes(R.dimen.font_size_small) - .typeface(FONT_OPEN_SANS) + .typeface(sAppTypeface.title()) .textColor(subtitleColor))) if (option.isSelectable) { @@ -150,7 +149,7 @@ object OptionLabelItemLayoutSpec { Text.create(context) .textRes(option.title) .textSizeRes(R.dimen.font_size_normal) - .typeface(FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textStyle(BOLD) .textColor(titleColor)) row.clickHandler(OptionItemLayout.onItemClick(context)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt index 3a906b68..c9ce93ce 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/BottomSheetBarSpec.kt @@ -14,7 +14,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetButton import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -37,7 +37,7 @@ object BottomSheetBarSpec { row.child( Text.create(context) .text(secondaryAction) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) @@ -50,7 +50,7 @@ object BottomSheetBarSpec { row.child( Text.create(context) .text(tertiaryAction) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textSizeRes(R.dimen.font_size_large) .paddingDip(YogaEdge.VERTICAL, 6f) .paddingDip(YogaEdge.HORIZONTAL, 16f) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt index e5171a8a..07633e90 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/CounterChooserSpec.kt @@ -11,7 +11,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.ui.ThemeColorType @LayoutSpec @@ -32,7 +32,7 @@ object CounterChooserSpec { .child( Text.create(context) .text(value.toString()) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textSizeRes(R.dimen.font_size_xxxlarge) .paddingDip(YogaEdge.HORIZONTAL, 12f) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt index 75402277..d5dfe7d0 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/specs/GridSectionViewSpec.kt @@ -19,7 +19,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme -import com.maubis.scarlet.base.config.CoreConfig +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.ui.ThemeColorType data class GridSectionItem( @@ -66,7 +66,7 @@ object GridOptionSpec { Text.create(context) .textRes(option.label) .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textSizeRes(R.dimen.font_size_small) .paddingDip(YogaEdge.VERTICAL, 8f) .paddingDip(YogaEdge.HORIZONTAL, 16f) @@ -101,7 +101,7 @@ object GridSectionViewSpec { column.child( Text.create(context) .textRes(section.title) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .textSizeRes(R.dimen.font_size_normal) .maxLines(1) .ellipsize(TextUtils.TruncateAt.END) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt new file mode 100644 index 00000000..33d90c6e --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt @@ -0,0 +1,21 @@ +package com.maubis.scarlet.base.support.ui.font + +import android.content.Context +import android.graphics.Typeface +import android.support.v4.content.res.ResourcesCompat +import com.maubis.scarlet.base.R + +class TypefaceController(context: Context) { + private val mFontMontserrat: Typeface = ResourcesCompat.getFont(context, R.font.monserrat) ?: Typeface.DEFAULT + private val mFontMontserratMedium: Typeface = ResourcesCompat.getFont(context, R.font.monserrat_medium) ?: Typeface.DEFAULT + private val mFontMontserratBold: Typeface = ResourcesCompat.getFont(context, R.font.monserrat_bold) ?: Typeface.DEFAULT + private val mFontOpenSource: Typeface = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT + + fun heading(): Typeface = mFontMontserratBold + + fun subHeading(): Typeface = mFontMontserratMedium + + fun title(): Typeface = mFontMontserrat + + fun text(): Typeface = mFontOpenSource +} \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt index adb24657..e9b22229 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/utils/FlavorUtils.kt @@ -1,8 +1,6 @@ package com.maubis.scarlet.base.support.utils import android.content.Context -import com.maubis.scarlet.base.config.ApplicationBase -import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppFlavor import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import kotlinx.coroutines.Dispatchers diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt index 016baae8..b9334aea 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveActivitySpecs.kt @@ -21,6 +21,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.LithoCircleDrawable @@ -52,7 +53,7 @@ object GDriveRootViewSpec { .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.google_drive_page_login_firebase_button) .textAlignment(Layout.Alignment.ALIGN_CENTER) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .alpha(if (sFirebaseKilled) 0.5f else 1f) .clickHandler(GDriveRootView.onFirebaseClick(context))) .child( @@ -74,7 +75,7 @@ object GDriveRootViewSpec { .textRes(buttonTitle) .textAlignment(Layout.Alignment.ALIGN_CENTER) .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .clickHandler(GDriveRootView.onGoogleClickEvent(context))) .build() } @@ -102,13 +103,13 @@ object GDriveContentViewSpec { .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_login_title) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .typeface(sAppTypeface.heading())) .child( Text.create(context) .textSizeRes(R.dimen.font_size_large) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_login_details) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .child( GDriveIconView.create(context) .marginDip(YogaEdge.TOP, 24f) @@ -148,7 +149,7 @@ object GDriveIconViewSpec { .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .build() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt index 18ee9409..39f50f60 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDriveLogoutActivitySpecs.kt @@ -17,6 +17,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -56,7 +57,7 @@ object GDriveLogoutRootViewSpec { .textRes(buttonTitle) .textAlignment(Layout.Alignment.ALIGN_CENTER) .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .clickHandler(GDriveLogoutRootView.onLogoutClick(context))) .build() } @@ -79,13 +80,13 @@ object GDriveLogoutContentViewSpec { .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.google_drive_page_logout_title) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .typeface(sAppTypeface.heading())) .child( Text.create(context) .textSizeRes(R.dimen.font_size_large) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.google_drive_page_logout_details) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .child( GDriveIconView.create(context) .marginDip(YogaEdge.TOP, 24f) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt index 158bc32e..28d30bd4 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/drive/GDrivePendingBottomSheet.kt @@ -25,6 +25,7 @@ import com.google.gson.Gson import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.export.data.getExportableNoteMeta import com.maubis.scarlet.base.note.getFullText @@ -66,7 +67,7 @@ object PendingItemIconSpec { Text.create(context) .text(label) .textSizeRes(R.dimen.font_size_xsmall) - .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .typeface(sAppTypeface.subHeading()) .textColor(secondaryColor)) .build() } @@ -140,7 +141,7 @@ object PendingItemLayoutSpec { .maxLines(1) .ellipsize(TextUtils.TruncateAt.END) .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .textColor(subtitleColor))) .child( Text.create(context) @@ -150,7 +151,7 @@ object PendingItemLayoutSpec { .marginDip(YogaEdge.ALL, 8f) .ellipsize(TextUtils.TruncateAt.MIDDLE) .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_MONSERRAT) + .typeface(sAppTypeface.title()) .backgroundRes(R.drawable.pending_note_background) .textColor(subtitleColor)) .child( @@ -162,13 +163,13 @@ object PendingItemLayoutSpec { .textRes(R.string.pending_backup_local_state) .textSizeRes(R.dimen.font_size_small) .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .typeface(sAppTypeface.subHeading()) .textColor(hintColor)) .child( Text.create(context) .textRes(localState) .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .paddingDip(YogaEdge.HORIZONTAL, 6f) .paddingDip(YogaEdge.VERTICAL, 2f) .marginDip(YogaEdge.HORIZONTAL, 4f) @@ -178,7 +179,7 @@ object PendingItemLayoutSpec { Text.create(context) .text(localUpdateTime) .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .paddingDip(YogaEdge.HORIZONTAL, 6f) .paddingDip(YogaEdge.VERTICAL, 2f) .backgroundRes(R.drawable.pending_note_background) @@ -193,13 +194,13 @@ object PendingItemLayoutSpec { .textRes(R.string.pending_backup_remote_state) .textSizeRes(R.dimen.font_size_small) .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT_MEDIUM) + .typeface(sAppTypeface.subHeading()) .textColor(hintColor)) .child( Text.create(context) .textRes(remoteState) .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .paddingDip(YogaEdge.HORIZONTAL, 6f) .paddingDip(YogaEdge.VERTICAL, 2f) .marginDip(YogaEdge.HORIZONTAL, 4f) @@ -209,7 +210,7 @@ object PendingItemLayoutSpec { Text.create(context) .text(remoteUpdateTime) .textSizeRes(R.dimen.font_size_small) - .typeface(CoreConfig.FONT_OPEN_SANS) + .typeface(sAppTypeface.text()) .paddingDip(YogaEdge.HORIZONTAL, 6f) .paddingDip(YogaEdge.VERTICAL, 2f) .backgroundRes(R.drawable.pending_note_background) diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt index e3273ddc..0a4ce18f 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseActivitySpecs.kt @@ -20,6 +20,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.specs.color import com.maubis.scarlet.base.support.ui.LithoCircleDrawable @@ -61,7 +62,7 @@ object FirebaseRootViewSpec { .textRes(buttonTitle) .textAlignment(Layout.Alignment.ALIGN_CENTER) .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .clickHandler(FirebaseRootView.onGoogleClickEvent(context))) .build() } @@ -84,13 +85,13 @@ object FirebaseContentViewSpec { .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.firebase_page_login_title) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .typeface(sAppTypeface.heading())) .child( Text.create(context) .textSizeRes(R.dimen.font_size_large) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_page_important_details) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .child( FirebaseIconView.create(context) .marginDip(YogaEdge.TOP, 24f) @@ -130,7 +131,7 @@ object FirebaseIconViewSpec { .textAlignment(Layout.Alignment.ALIGN_CENTER) .textSizeRes(R.dimen.font_size_normal) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .build() } } diff --git a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt index 13c9062b..5b06c1eb 100644 --- a/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt +++ b/scarlet/src/main/java/com/bijoysingh/quicknote/firebase/activity/FirebaseRemovalActivitySpecs.kt @@ -17,6 +17,7 @@ import com.facebook.yoga.YogaAlign import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -56,7 +57,7 @@ object FirebaseRemovalRootViewSpec { .textRes(buttonTitle) .textAlignment(Layout.Alignment.ALIGN_CENTER) .flexGrow(1f) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .clickHandler(FirebaseRemovalRootView.onLogoutClickEvent(context))) .build() } @@ -79,13 +80,13 @@ object FirebaseRemovalContentViewSpec { .textSizeRes(R.dimen.font_size_xxlarge) .textRes(R.string.firebase_removal_page_login_title) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .typeface(sAppTypeface.heading())) .child( Text.create(context) .textSizeRes(R.dimen.font_size_large) .textColor(sAppTheme.get(ThemeColorType.SECONDARY_TEXT)) .textRes(R.string.firebase_removal_page_important_details) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .child( Row.create(context) .marginDip(YogaEdge.TOP, 24f) @@ -110,7 +111,7 @@ object FirebaseRemovalContentViewSpec { .textSizeRes(R.dimen.font_size_normal) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_whats_next_details) - .typeface(CoreConfig.FONT_MONSERRAT_BOLD)) + .typeface(sAppTypeface.heading())) .child( Text.create(context) .backgroundRes(R.drawable.secondary_rounded_bg) @@ -120,7 +121,7 @@ object FirebaseRemovalContentViewSpec { .textSizeRes(R.dimen.font_size_normal) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_remove_details) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .child( Text.create(context) .backgroundRes(R.drawable.secondary_rounded_bg) @@ -130,7 +131,7 @@ object FirebaseRemovalContentViewSpec { .textSizeRes(R.dimen.font_size_normal) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT)) .textRes(R.string.firebase_removal_page_next_details) - .typeface(CoreConfig.FONT_MONSERRAT)) + .typeface(sAppTypeface.title())) .build() } } \ No newline at end of file From 8ec4d79481cd1a458721efb2d9b3a72fbbd0349a Mon Sep 17 00:00:00 2001 From: Bijoy Singh Kochar Date: Sun, 3 Nov 2019 18:27:24 +0000 Subject: [PATCH 114/134] [Typeface] Adding option to choose font --- .../scarlet/base/MainActivityExtensions.kt | 18 +- .../maubis/scarlet/base/config/CoreConfig.kt | 3 +- .../export/recycler/FileImportViewHolder.kt | 8 + .../sheet/ExternalFolderSyncBottomSheet.kt | 1 + .../export/sheet/PermissionBottomSheet.kt | 3 + .../recycler/InformationRecyclerHolder.kt | 5 + .../recycler/ToolbarMainRecyclerHolder.kt | 6 +- .../base/main/sheets/ExceptionBottomSheet.kt | 2 + .../note/actions/NoteOptionsBottomSheet.kt | 11 +- .../creation/sheet/MarkdownHelpBottomSheet.kt | 2 + .../base/note/folder/FolderRecyclerHolder.kt | 4 + .../formats/recycler/FormatTextViewHolder.kt | 3 + .../formats/recycler/FormatViewHolderBase.kt | 20 ++- .../recycler/NoteRecyclerViewHolderBase.kt | 2 + .../tag/view/TagsAndColorPickerViewHolder.kt | 2 + .../security/sheets/NoPincodeBottomSheet.kt | 3 + .../security/sheets/PincodeBottomSheet.kt | 1 + .../base/settings/sheet/AboutUsBottomSheet.kt | 3 + .../settings/sheet/FontSizeBottomSheet.kt | 2 + .../settings/sheet/OpenSourceBottomSheet.kt | 2 + .../sheet/TypefacePickerBottomSheet.kt | 156 ++++++++++++++++++ .../sheet/UISettingsOptionsBottomSheet.kt | 8 + .../support/sheets/GridBottomSheetBase.kt | 2 + .../support/ui/font/TypefaceController.kt | 86 +++++++++- base/src/main/res/drawable/icon_typeface.png | Bin 0 -> 646 bytes base/src/main/res/font/mono_bold.ttf | Bin 0 -> 109500 bytes base/src/main/res/font/mono_bold_xml.xml | 7 + base/src/main/res/font/mono_medium.ttf | Bin 0 -> 109292 bytes base/src/main/res/font/mono_medium_xml.xml | 7 + base/src/main/res/font/mono_regular.ttf | Bin 0 -> 109212 bytes base/src/main/res/font/mono_regular_xml.xml | 7 + base/src/main/res/font/serif_bold.ttf | Bin 0 -> 348704 bytes base/src/main/res/font/serif_bold_xml.xml | 7 + base/src/main/res/font/serif_regular.ttf | Bin 0 -> 330000 bytes base/src/main/res/font/serif_regular_xml.xml | 7 + base/src/main/res/layout/item_format_code.xml | 2 - .../main/res/layout/item_format_heading.xml | 2 - base/src/main/res/layout/item_import_file.xml | 2 - .../res/layout/layout_choose_sheet_item.xml | 3 +- .../main/res/layout/layout_home_tag_item.xml | 1 - .../res/layout/layout_option_sheet_item.xml | 2 - .../main/res/layout/toolbar_folder_bottom.xml | 1 - base/src/main/res/layout/toolbar_main.xml | 1 - .../res/layout/widget_layout_all_notes.xml | 1 - base/src/main/res/values-v23/styles.xml | 1 - base/src/main/res/values/strings.xml | 12 ++ base/src/main/res/values/styles.xml | 17 +- .../markdown/spannable/SpannableExtensions.kt | 51 +++--- .../com/maubis/markdown/spans/CodeSpan.kt | 2 +- .../maubis/markdown/spans/QuoteSegmentSpan.kt | 5 +- .../com/maubis/markdown/spans/SpanConfig.kt | 4 + 51 files changed, 423 insertions(+), 72 deletions(-) create mode 100644 base/src/main/java/com/maubis/scarlet/base/settings/sheet/TypefacePickerBottomSheet.kt create mode 100644 base/src/main/res/drawable/icon_typeface.png create mode 100644 base/src/main/res/font/mono_bold.ttf create mode 100644 base/src/main/res/font/mono_bold_xml.xml create mode 100644 base/src/main/res/font/mono_medium.ttf create mode 100644 base/src/main/res/font/mono_medium_xml.xml create mode 100644 base/src/main/res/font/mono_regular.ttf create mode 100644 base/src/main/res/font/mono_regular_xml.xml create mode 100644 base/src/main/res/font/serif_bold.ttf create mode 100644 base/src/main/res/font/serif_bold_xml.xml create mode 100644 base/src/main/res/font/serif_regular.ttf create mode 100644 base/src/main/res/font/serif_regular_xml.xml diff --git a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt index 75a37dea..e7dddc22 100644 --- a/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt +++ b/base/src/main/java/com/maubis/scarlet/base/MainActivityExtensions.kt @@ -3,15 +3,19 @@ package com.maubis.scarlet.base import android.content.Context import android.content.Intent import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.settings.sheet.ThemeColorPickerBottomSheet +import com.maubis.scarlet.base.settings.sheet.TypefacePickerBottomSheet import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.ui.font.sPreferenceTypeface import com.maubis.scarlet.base.support.ui.sAppThemeLabel const val INTENT_KEY_ADDITIONAL_ACTION = "additional_action" enum class MainActivityActions { NIL, - COLOR_PICKER; + COLOR_PICKER, + TYPEFACE_PICKER; fun intent(context: Context): Intent { val intent = Intent(context, MainActivity::class.java) @@ -55,5 +59,17 @@ fun MainActivity.performAction(action: MainActivityActions) { } }) } + MainActivityActions.TYPEFACE_PICKER -> { + openSheet(this, TypefacePickerBottomSheet().apply { + this.onTypefaceChange = { typeface -> + if (sPreferenceTypeface != typeface.name) { + sPreferenceTypeface = typeface.name + sAppTypeface.notifyChange(activity) + activity.startActivity(MainActivityActions.TYPEFACE_PICKER.intent(activity)) + activity.finish() + } + } + }) + } } } \ No newline at end of file diff --git a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt index 2acaad83..5c23b40b 100644 --- a/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt +++ b/base/src/main/java/com/maubis/scarlet/base/config/CoreConfig.kt @@ -8,6 +8,7 @@ import com.github.ajalt.reprint.core.Reprint import com.maubis.markdown.MarkdownConfig.Companion.config import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.instance +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.auth.IAuthenticator import com.maubis.scarlet.base.config.remote.IRemoteConfigFetcher import com.maubis.scarlet.base.core.folder.IFolderActor @@ -26,8 +27,6 @@ abstract class CoreConfig(context: Context) { init { Reprint.initialize(context) - config.spanConfig.headingTypeface = ResourcesCompat.getFont(context, R.font.monserrat) - ?: Typeface.DEFAULT } abstract fun database(): AppDatabase diff --git a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt index 5c76f956..1f85aeb1 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/recycler/FileImportViewHolder.kt @@ -9,6 +9,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.LocaleManager import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme import com.maubis.scarlet.base.export.activity.ImportNoteActivity import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -34,9 +35,16 @@ class FileImportViewHolder(context: Context, root: View) override fun populate(data: RecyclerItem, extra: Bundle?) { val item = data as FileRecyclerItem fileName.text = item.name + fileName.typeface = ApplicationBase.sAppTypeface.title() + filePath.text = getPath(item) + filePath.typeface = ApplicationBase.sAppTypeface.text() + fileDate.text = getSubtitleText(item.file) + fileDate.typeface = ApplicationBase.sAppTypeface.text() + fileSize.text = getMetaText(item.file) + fileSize.typeface = ApplicationBase.sAppTypeface.text() root.setOnClickListener { (context as ImportNoteActivity).select(item) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt index e8b6007d..30d364c2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/ExternalFolderSyncBottomSheet.kt @@ -36,6 +36,7 @@ class ExternalFolderSyncBottomSheet : LithoBottomSheet() { .child( Text.create(componentContext) .textSizeRes(R.dimen.font_size_large) + .typeface(sAppTypeface.text()) .textRes(R.string.import_export_layout_folder_sync_description) .paddingDip(YogaEdge.HORIZONTAL, 20f) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) diff --git a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt index 2c714e0e..89ffb887 100644 --- a/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/export/sheet/PermissionBottomSheet.kt @@ -7,7 +7,9 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.export.support.PermissionUtils import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle @@ -28,6 +30,7 @@ class PermissionBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .textRes(R.string.permission_layout_give_permission_details) diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerHolder.kt index ef32b299..04a16741 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/InformationRecyclerHolder.kt @@ -7,6 +7,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.recycler.RecyclerItem class InformationRecyclerHolder(context: Context, itemView: View) : RecyclerViewHolder(context, itemView) { @@ -19,8 +20,12 @@ class InformationRecyclerHolder(context: Context, itemView: View) : RecyclerView return } title.setText(data.title) + title.label.typeface = sAppTypeface.title() title.setImageResource(data.icon) + text.setText(data.source) + text.typeface = sAppTypeface.text() + itemView.setOnClickListener { data.function() } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt index 2b4398e8..3eb843b8 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/recycler/ToolbarMainRecyclerHolder.kt @@ -10,8 +10,11 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.maubis.scarlet.base.BuildConfig import com.maubis.scarlet.base.MainActivity +import com.maubis.scarlet.base.MainActivityActions import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface +import com.maubis.scarlet.base.performAction import com.maubis.scarlet.base.settings.sheet.InternalSettingsOptionsBottomSheet import com.maubis.scarlet.base.settings.sheet.SettingsOptionsBottomSheet import com.maubis.scarlet.base.support.recycler.RecyclerItem @@ -39,6 +42,7 @@ class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerView val titleColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) toolbarTitle.setTextColor(titleColor) + toolbarTitle.typeface = sAppTypeface.heading() val toolbarIconColor = sAppTheme.get(ThemeColorType.SECONDARY_TEXT) toolbarIconSearch.setColorFilter(toolbarIconColor) @@ -47,7 +51,7 @@ class ToolbarMainRecyclerHolder(context: Context, itemView: View) : RecyclerView toolbarIconDebug.visibility = visibility(BuildConfig.DEBUG) toolbarIconDebug.setColorFilter(toolbarIconColor) toolbarIconDebug.setOnClickListener { - openSheet((context as MainActivity), InternalSettingsOptionsBottomSheet()) + (context as MainActivity).performAction(MainActivityActions.TYPEFACE_PICKER) } } } diff --git a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt index ed432ba9..12ebf806 100644 --- a/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/main/sheets/ExceptionBottomSheet.kt @@ -12,6 +12,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -31,6 +32,7 @@ class ExceptionBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child( Text.create(componentContext) + .typeface(sAppTypeface.code()) .textSizeRes(R.dimen.font_size_small) .text(Markdown.render("```\n${Log.getStackTraceString(exception)}\n```", true)) .marginDip(YogaEdge.BOTTOM, 16f) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt index a9330213..39b79cf5 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/actions/NoteOptionsBottomSheet.kt @@ -15,6 +15,7 @@ import com.github.bijoysingh.starter.util.RandomHelper import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.core.note.NoteBuilder import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.core.note.getNoteState @@ -100,15 +101,22 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { val tagCardLayout = dialog.findViewById(R.id.tag_card_layout) val selectCardLayout = dialog.findViewById(R.id.select_notes_layout) + val selectCardTitle = dialog.findViewById(R.id.select_notes_title) + selectCardTitle.typeface = sAppTypeface.title() + val selectCardSubtitle = dialog.findViewById(R.id.select_notes_subtitle) + selectCardSubtitle.typeface = sAppTypeface.title() + val tags = tagCardLayout.findViewById(R.id.tags_content) + tags.typeface = sAppTypeface.title() val tagSubtitle = tagCardLayout.findViewById(R.id.tags_subtitle) + tagSubtitle.typeface = sAppTypeface.title() + val tagContent = note.getTagString() if (tagContent.isNotBlank()) { GlobalScope.launch(Dispatchers.Main) { val text = GlobalScope.async(Dispatchers.IO) { Markdown.renderSegment(tagContent, true) } tags.visibility = View.VISIBLE tagSubtitle.visibility = View.GONE - tags.text = text.await() groupCardLayout.orientation = VERTICAL @@ -117,6 +125,7 @@ class NoteOptionsBottomSheet() : GridBottomSheetBase() { val margin = activity.resources.getDimension(R.dimen.spacing_xxsmall).toInt() params.setMargins(margin, margin, margin, margin) tagCardLayout.layoutParams = params + selectCardLayout.layoutParams = params } } diff --git a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt index 6c39435f..cae6b72c 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/creation/sheet/MarkdownHelpBottomSheet.kt @@ -9,6 +9,7 @@ import com.facebook.yoga.YogaEdge import com.maubis.markdown.Markdown import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.ui.ThemeColorType @@ -27,6 +28,7 @@ class MarkdownHelpBottomSheet : LithoBottomSheet() { column .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .text(Markdown.render(it)) .textSizeRes(R.dimen.font_size_normal) .marginDip(YogaEdge.HORIZONTAL, 20f) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerHolder.kt index 3bbc11cc..d5c523ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/folder/FolderRecyclerHolder.kt @@ -8,6 +8,7 @@ import android.widget.TextView import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.uibasics.views.UITextView import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.recycler.RecyclerItem class FolderRecyclerHolder(context: Context, view: View) : RecyclerViewHolder(context, view) { @@ -28,13 +29,16 @@ class FolderRecyclerHolder(context: Context, view: View) : RecyclerViewHolder(context, view) { @@ -96,7 +101,18 @@ abstract class FormatViewHolderBase(context: Context, view: View) : RecyclerView iconColor = iconColor, hintTextColor = hintTextColor, accentColor = linkColor, - noteUUID = extra?.getString(INTENT_KEY_NOTE_ID) ?: "default") + noteUUID = extra?.getString(INTENT_KEY_NOTE_ID) ?: "default", + typeface = when (data.formatType) { + FormatType.HEADING -> sAppTypeface.subHeading() + FormatType.SUB_HEADING -> sAppTypeface.title() + FormatType.HEADING_3 -> sAppTypeface.title() + FormatType.CODE -> sAppTypeface.code() + else -> sAppTypeface.text() + }, + typefaceStyle = when (data.formatType) { + FormatType.HEADING, FormatType.SUB_HEADING, FormatType.HEADING_3 -> Typeface.BOLD + else -> Typeface.NORMAL + }) populate(data, config) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt index 25e87fca..5bf250c4 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/recycler/NoteRecyclerViewHolderBase.kt @@ -12,6 +12,7 @@ import com.github.bijoysingh.starter.recyclerview.RecyclerViewHolder import com.github.bijoysingh.starter.util.TextUtils import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppImageStorage +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.core.note.NoteState import com.maubis.scarlet.base.database.room.note.Note import com.maubis.scarlet.base.note.isNoteLockedButAppUnlocked @@ -116,6 +117,7 @@ open class NoteRecyclerViewHolderBase(context: Context, view: View) : RecyclerVi } private fun setMetaText(note: NoteRecyclerItem) { + tags.typeface = sAppTypeface.text() when { !TextUtils.isNullOrEmpty(note.tagsSource) -> { tags.setTextColor(note.tagsColor) diff --git a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt index b3a6725d..b98c8ef2 100644 --- a/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt +++ b/base/src/main/java/com/maubis/scarlet/base/note/tag/view/TagsAndColorPickerViewHolder.kt @@ -6,6 +6,7 @@ import android.widget.TextView import com.google.android.flexbox.FlexboxLayout import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.config.CoreConfig.Companion.notesDb import com.maubis.scarlet.base.config.CoreConfig.Companion.tagsDb import com.maubis.scarlet.base.database.room.tag.Tag @@ -54,6 +55,7 @@ class TagsAndColorPickerViewHolder( } text.text = it.title + text.typeface = sAppTypeface.title() tagView.setOnClickListener { onTagClick(tag) } diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt index aa79321b..6b701e14 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/NoPincodeBottomSheet.kt @@ -7,8 +7,10 @@ import com.facebook.litho.ComponentContext import com.facebook.litho.widget.Text import com.facebook.yoga.YogaEdge import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -35,6 +37,7 @@ class NoPincodeBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .textRes(R.string.no_pincode_sheet_details) .marginDip(YogaEdge.BOTTOM, 16f) diff --git a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt index c6a66db4..13ae85ab 100644 --- a/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/security/sheets/PincodeBottomSheet.kt @@ -84,6 +84,7 @@ object PincodeSheetViewSpec { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child( Text.create(context) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .textRes(R.string.app_lock_details) .marginDip(YogaEdge.BOTTOM, 16f) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt index 9be181ff..7f7d4e47 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/AboutUsBottomSheet.kt @@ -44,6 +44,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutUsDetails) @@ -57,6 +58,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(aboutAppDetails) @@ -70,6 +72,7 @@ class AboutUsBottomSheet : LithoBottomSheet() { .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(version) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt index d3a2a955..b1271814 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/FontSizeBottomSheet.kt @@ -10,6 +10,7 @@ import com.maubis.scarlet.base.MainActivity import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppPreferences import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.sheets.LithoBottomSheet import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle import com.maubis.scarlet.base.support.specs.BottomSheetBar @@ -41,6 +42,7 @@ class FontSizeBottomSheet : LithoBottomSheet() { Text.create(componentContext) .textSizeDip(sEditorTextSize.toFloat()) .marginDip(YogaEdge.BOTTOM, 16f) + .typeface(sAppTypeface.text()) .textRes(R.string.note_option_font_size_example) .textColor(sAppTheme.get(ThemeColorType.TERTIARY_TEXT))) .child(CounterChooser.create(componentContext) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt index 9db8f77a..bddba616 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/OpenSourceBottomSheet.kt @@ -36,6 +36,7 @@ class OpenSourceBottomSheet : LithoBottomSheet() { .marginDip(YogaEdge.HORIZONTAL, 0f)) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 16f) .text(openSourceDetails) @@ -49,6 +50,7 @@ class OpenSourceBottomSheet : LithoBottomSheet() { .textColor(sAppTheme.get(ThemeColorType.SECTION_HEADER))) .child( Text.create(componentContext) + .typeface(sAppTypeface.text()) .textSizeRes(R.dimen.font_size_large) .marginDip(YogaEdge.BOTTOM, 4f) .text(Markdown.render(LIBRARY_DETAILS_MD, true)) diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/TypefacePickerBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/TypefacePickerBottomSheet.kt new file mode 100644 index 00000000..b80cca79 --- /dev/null +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/TypefacePickerBottomSheet.kt @@ -0,0 +1,156 @@ +package com.maubis.scarlet.base.settings.sheet + +import android.app.Dialog +import com.facebook.litho.ClickEvent +import com.facebook.litho.Column +import com.facebook.litho.Component +import com.facebook.litho.ComponentContext +import com.facebook.litho.Row +import com.facebook.litho.annotations.LayoutSpec +import com.facebook.litho.annotations.OnCreateLayout +import com.facebook.litho.annotations.OnEvent +import com.facebook.litho.annotations.Prop +import com.facebook.litho.widget.Text +import com.facebook.yoga.YogaAlign +import com.facebook.yoga.YogaEdge +import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface +import com.maubis.scarlet.base.main.sheets.InstallProUpsellBottomSheet +import com.maubis.scarlet.base.support.sheets.LithoBottomSheet +import com.maubis.scarlet.base.support.sheets.getLithoBottomSheetTitle +import com.maubis.scarlet.base.support.sheets.openSheet +import com.maubis.scarlet.base.support.specs.BottomSheetBar +import com.maubis.scarlet.base.support.specs.EmptySpec +import com.maubis.scarlet.base.support.ui.LithoCircleDrawable +import com.maubis.scarlet.base.support.ui.ThemeColorType +import com.maubis.scarlet.base.support.ui.ThemedActivity +import com.maubis.scarlet.base.support.ui.font.TypefaceController +import com.maubis.scarlet.base.support.ui.font.sPreferenceTypeface +import com.maubis.scarlet.base.support.utils.FlavorUtils + +@LayoutSpec +object TypefacePickerItemSpec { + @OnCreateLayout + fun onCreate( + context: ComponentContext, + @Prop typeface: TypefaceController.TypefaceType, + @Prop isDisabled: Boolean, + @Prop isSelected: Boolean): Component { + + val typefaceSet = sAppTypeface.getSetForType(context.androidContext, typeface) + val fontColor = when (isSelected) { + true -> sAppTheme.get(ThemeColorType.ACCENT_TEXT) + false -> sAppTheme.get(ThemeColorType.SECONDARY_TEXT) + } + val content = Column.create(context) + .paddingDip(YogaEdge.ALL, 24f) + .child( + Row.create(context) + .child( + Text.create(context) + .text("Abc") + .textSizeRes(R.dimen.font_size_xlarge) + .textColor(fontColor) + .typeface(typefaceSet.heading)) + .child( + Text.create(context) + .text("defg") + .textSizeRes(R.dimen.font_size_xlarge) + .textColor(fontColor) + .typeface(typefaceSet.subHeading))) + .child( + Text.create(context) + .text("hijklmnop") + .textSizeRes(R.dimen.font_size_large) + .textColor(fontColor) + .typeface(typefaceSet.title)) + .child( + Text.create(context) + .text("qrstuvwxyz\n0123456789") + .textSizeRes(R.dimen.font_size_normal) + .textColor(fontColor) + .typeface(typefaceSet.text)) + .background(LithoCircleDrawable(sAppTheme.get(ThemeColorType.BACKGROUND), 255, true)) + if (isDisabled) { + content.alpha(0.6f) + } + + val data = Column.create(context) + .alignSelf(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .alignItems(YogaAlign.CENTER) + .child(content) + .child( + Text.create(context) + .textRes(typeface.title) + .textSizeRes(R.dimen.font_size_normal) + .textColor(sAppTheme.get(ThemeColorType.PRIMARY_TEXT)) + .typeface(sAppTypeface.title()) + .marginDip(YogaEdge.TOP, 12f)) + + val row = Row.create(context) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .alignContent(YogaAlign.CENTER) + .child(data) + row.clickHandler(ThemeColorPickerItem.onItemClick(context)) + return row.build() + } + + @OnEvent(ClickEvent::class) + fun onItemClick( + context: ComponentContext, + @Prop typeface: TypefaceController.TypefaceType, + @Prop isDisabled: Boolean, + @Prop onTypefaceSelected: (TypefaceController.TypefaceType) -> Unit) { + if (isDisabled) { + openSheet(context.androidContext as ThemedActivity, InstallProUpsellBottomSheet()) + return + } + onTypefaceSelected(typeface) + } +} + +class TypefacePickerBottomSheet : LithoBottomSheet() { + + var onTypefaceChange: (TypefaceController.TypefaceType) -> Unit = {} + + override fun getComponent(componentContext: ComponentContext, dialog: Dialog): Component { + val column = Column.create(componentContext) + .widthPercent(100f) + .paddingDip(YogaEdge.VERTICAL, 8f) + .paddingDip(YogaEdge.HORIZONTAL, 20f) + .child( + getLithoBottomSheetTitle(componentContext) + .textRes(R.string.typeface_page_title) + .marginDip(YogaEdge.HORIZONTAL, 0f)) + + var flex: Row.Builder? = null + TypefaceController.TypefaceType.values().forEachIndexed { index, typeface -> + if (index % 2 == 0) { + column.child(flex) + flex = Row.create(componentContext) + .widthPercent(100f) + .alignItems(YogaAlign.CENTER) + .paddingDip(YogaEdge.VERTICAL, 12f) + } + flex?.child( + TypefacePickerItem.create(componentContext) + .typeface(typeface) + .isDisabled(FlavorUtils.isLite() && !typeface.isLiteEnabled) + .isSelected(sPreferenceTypeface == typeface.name) + .onTypefaceSelected { newTypeface -> onTypefaceChange(newTypeface) } + .flexGrow(1f)) + } + column.child(flex) + + column.child(EmptySpec.create(componentContext).widthPercent(100f).heightDip(24f)) + column.child(BottomSheetBar.create(componentContext) + .primaryActionRes(R.string.import_export_layout_exporting_done) + .onPrimaryClick { + dismiss() + }.paddingDip(YogaEdge.VERTICAL, 8f)) + return column.build() + } +} diff --git a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt index 70536eb4..fcc76f53 100644 --- a/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt +++ b/base/src/main/java/com/maubis/scarlet/base/settings/sheet/UISettingsOptionsBottomSheet.kt @@ -30,6 +30,14 @@ class UISettingsOptionsBottomSheet : LithoOptionBottomSheet() { activity.performAction(MainActivityActions.COLOR_PICKER) } )) + options.add(LithoOptionsItem( + title = R.string.home_option_typeface, + subtitle = R.string.home_option_typeface_subtitle, + icon = R.drawable.icon_typeface, + listener = { + activity.performAction(MainActivityActions.TYPEFACE_PICKER) + } + )) val isTablet = resources.getBoolean(R.bool.is_tablet) options.add( LithoOptionsItem( diff --git a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt index 08c31d64..c8e168b6 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/sheets/GridBottomSheetBase.kt @@ -7,6 +7,7 @@ import android.widget.TextView import com.github.bijoysingh.uibasics.views.UILabelView import com.maubis.scarlet.base.R import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTheme +import com.maubis.scarlet.base.config.ApplicationBase.Companion.sAppTypeface import com.maubis.scarlet.base.support.option.OptionsItem import com.maubis.scarlet.base.support.ui.ThemeColorType import com.maubis.scarlet.base.support.ui.ThemedBottomSheetFragment @@ -55,6 +56,7 @@ abstract class GridBottomSheetBase : ThemedBottomSheetFragment() { } val contentView = View.inflate(context, R.layout.layout_grid_item, null) as UILabelView + contentView.label.typeface = sAppTypeface.title() contentView.setText(option.title) contentView.setImageResource(option.icon) contentView.setTextColor(getOptionsTitleColor(option.selected)) diff --git a/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt b/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt index 33d90c6e..0dde4776 100644 --- a/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt +++ b/base/src/main/java/com/maubis/scarlet/base/support/ui/font/TypefaceController.kt @@ -3,19 +3,89 @@ package com.maubis.scarlet.base.support.ui.font import android.content.Context import android.graphics.Typeface import android.support.v4.content.res.ResourcesCompat +import com.maubis.markdown.MarkdownConfig import com.maubis.scarlet.base.R +import com.maubis.scarlet.base.config.ApplicationBase + +const val KEY_PREFERENCE_TYPEFACE = "typeface_setting" +var sPreferenceTypeface: String + get() = ApplicationBase.sAppPreferences.get(KEY_PREFERENCE_TYPEFACE, TypefaceController.TypefaceType.APP_DEFAULT.name) + set(value) = ApplicationBase.sAppPreferences.put(KEY_PREFERENCE_TYPEFACE, value) class TypefaceController(context: Context) { - private val mFontMontserrat: Typeface = ResourcesCompat.getFont(context, R.font.monserrat) ?: Typeface.DEFAULT - private val mFontMontserratMedium: Typeface = ResourcesCompat.getFont(context, R.font.monserrat_medium) ?: Typeface.DEFAULT - private val mFontMontserratBold: Typeface = ResourcesCompat.getFont(context, R.font.monserrat_bold) ?: Typeface.DEFAULT - private val mFontOpenSource: Typeface = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT + enum class TypefaceType(val title: Int, val isLiteEnabled: Boolean) { + APP_DEFAULT(R.string.typeface_title_app_default, true), + OS_DEFAULT(R.string.typeface_title_os_default, true), + MONOSPACE(R.string.typeface_title_monospace, false), + SERIF_TITLE(R.string.typeface_title_serif, false), + } + + data class TypefaceSet( + val heading: Typeface = Typeface.DEFAULT, + val subHeading: Typeface = Typeface.DEFAULT, + val title: Typeface = Typeface.DEFAULT, + val text: Typeface = Typeface.DEFAULT, + val code: Typeface = Typeface.MONOSPACE + ) + + private var mTypefaceSet: TypefaceSet = TypefaceSet() + + init { + notifyChange(context) + } + + fun notifyChange(context: Context) { + mTypefaceSet = getSetForType(context, getTypefaceSetting()) + setMarkdownConfig() + } + + fun getSetForType(context: Context, typefaceType: TypefaceType): TypefaceSet { + return when (typefaceType) { + TypefaceType.APP_DEFAULT -> TypefaceSet( + heading = ResourcesCompat.getFont(context, R.font.monserrat_bold) ?: Typeface.DEFAULT, + subHeading = ResourcesCompat.getFont(context, R.font.monserrat_medium) ?: Typeface.DEFAULT, + title = ResourcesCompat.getFont(context, R.font.monserrat) ?: Typeface.DEFAULT, + text = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT, + code = Typeface.MONOSPACE) + TypefaceType.OS_DEFAULT -> TypefaceSet() + TypefaceType.MONOSPACE -> TypefaceSet( + heading = ResourcesCompat.getFont(context, R.font.mono_bold_xml) ?: Typeface.MONOSPACE, + subHeading = ResourcesCompat.getFont(context, R.font.mono_medium_xml) ?: Typeface.MONOSPACE, + title = ResourcesCompat.getFont(context, R.font.mono_regular_xml) ?: Typeface.MONOSPACE, + text = ResourcesCompat.getFont(context, R.font.mono_regular_xml) ?: Typeface.MONOSPACE, + code = Typeface.MONOSPACE) + TypefaceType.SERIF_TITLE -> TypefaceSet( + heading = ResourcesCompat.getFont(context, R.font.serif_bold_xml) ?: Typeface.SERIF, + subHeading = ResourcesCompat.getFont(context, R.font.serif_bold_xml) ?: Typeface.SERIF, + title = ResourcesCompat.getFont(context, R.font.serif_regular_xml) ?: Typeface.SERIF, + text = ResourcesCompat.getFont(context, R.font.open_sans) ?: Typeface.DEFAULT, + code = Typeface.MONOSPACE) + } + } + + private fun setMarkdownConfig() { + MarkdownConfig.config.spanConfig.headingTypeface = subHeading() + MarkdownConfig.config.spanConfig.heading2Typeface = title() + MarkdownConfig.config.spanConfig.heading3Typeface = title() + MarkdownConfig.config.spanConfig.textTypeface = text() + MarkdownConfig.config.spanConfig.codeTypeface = code() + } + + private fun getTypefaceSetting(): TypefaceType { + return try { + TypefaceType.valueOf(sPreferenceTypeface) + } catch (exception: Exception) { + TypefaceType.APP_DEFAULT + } + } + + fun heading(): Typeface = mTypefaceSet.heading - fun heading(): Typeface = mFontMontserratBold + fun subHeading(): Typeface = mTypefaceSet.subHeading - fun subHeading(): Typeface = mFontMontserratMedium + fun title(): Typeface = mTypefaceSet.title - fun title(): Typeface = mFontMontserrat + fun text(): Typeface = mTypefaceSet.text - fun text(): Typeface = mFontOpenSource + fun code(): Typeface = mTypefaceSet.code } \ No newline at end of file diff --git a/base/src/main/res/drawable/icon_typeface.png b/base/src/main/res/drawable/icon_typeface.png new file mode 100644 index 0000000000000000000000000000000000000000..0bf19d5ad6e6eb1d63573145cc7baf0b87ca266d GIT binary patch literal 646 zcmV;10(t$3P)&1g0&j82Zcq<#V4&TgU&VnqHiMQ)DA-^xsI-KF zDz<_y@s>XFfBx6=?{Upe&>&HCr@JUd+X;%Rk9&o)oaGh0oXkehc2VR2AiF5`nGdQW ziYWk07DZjtLDzUsAN2_V00MlX*L_UDY|t1{)Z&pEqL^SdD7h%M@yJ$DWH1@DPZY5U zDW+ck0!PdR)e}W;Lh31s=B9!IeEirD2-IUkqc6B^CTM~v^5B$H6f?~PWfa8>oTiE* zmx-VwyyzSPDd*7W6;65|)It>X2u&?fbo4stHeWb~KuT>D#jjom%@W0XG!L?SR}}NT z4azN%L=i88PV!0=mRX+jE~t}40lmEo`cAW_6hZ-R=7A`{KS*`*RiIhTW+ zbMd^uUc$3V6mN0O{h&UgNI^vWE{dV<2mK?8QA9*XQ6z9T$oU0aB_dAnk|?&j8&p*k zWr?WtqNwX`P|_$Z?;j_MwC)BK)cgEL#9ifmeOz^SgLd<&+{JgSCo;zIlHA2l9C9}Z z4$zjtTq3fLFp#+f{3ZEzg8ZOM*l_77TE+Rqtpt9NC78B^7bQ5pWHTmh=F5m|YOxO! g_UWb;2KjuxJEtr&L2h7TTmS$707*qoM6N<$f_ZEvOaK4? literal 0 HcmV?d00001 diff --git a/base/src/main/res/font/mono_bold.ttf b/base/src/main/res/font/mono_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..482f028aba834832768701ccd1557f82966d222c GIT binary patch literal 109500 zcmb5X2Vhg@xjufrGbC>;+mdX{OO|BGlI109cuU^f2HSW6-UBdThB14DO@J^0NfWX| z(gd22y_+qlD=Mpkhl7V0a-R#I7F4&km;L^;w}bJ14a2ZC&8=Cv&yp{v7&6g->wSaE z`X@dVt6W;5`maSR0JL8lQ*LO0^ zoIA%?4EATLcc4F=V+=p*j397b3@31nB}Fc0PIf|k ztXdH%3yQIjyiC@Z$=UNtSf|~lOyZQ>l-G_&r;+M-8+3K^^YG-%(@ZFQ81>-j^x~={8jdetAE}w5*x}0n= zdmL7kzGcb5{`P}o?%;Rc3#*&DV`%+mO-`B7Se%y?n>Kgp!M@IeBgMflzZ_IJae7@% ze}3!`BQUy)U-S0~uP{o+!g!btriUqU7xr|sEZ0S3ats^`i;)jPaO5&xKFDw!?qQgU zdPXJ_dNA}#p&{M(pFSuk!4i$J+H#XvB^{eCHIud5q>+`db~|Q68a(-bxJNJ&wN6*P zw7Pa_l}=Z+q^4$Rwf@-CI;KwH}`LFdS2V@hkpq%$u3XXBwC_?&#uJfg?gzW=b5-6CK2btMhG; zRLoIe$_Qb5IKr~U^)kZq{@+>bjg-r8>S6--a6Opu8m^(sJtxCbTW!hD$k)XrWyrBz z=}bG*(x`Ar6HaAG9_?^o!ew%`)+m$F=@)VxP9l$hs3oynj7se+VeH;R~C^M@`f_Se+4C_Ddn*lrD{XU!sL|j zh9)tShEwmkDTK^W^a>pRJ1!1GDf=aA=OU`Hb~{u zPV8+7Pyd&&N?gGv(3pNDAs zMQDHJW$njA@N#1b8wFuz)6$I2Btglo%xD;1wJzW8oalBHbgyvO^Vh8!ZpdH@_Wk4S z2lw^GoiHE$FOu?9{F5Z*-J|B?aeez9eET2!&^~h$bC&-J{|FPo#JklI;bEa6!9j8v z$HHt>V*(b(kRYXn7@PuU;|ywn$T`l)ri$yz$dvwlBUw`>t|DO%|L%8>u$%kdyoWp} z*4_1+9`U*DWQ};kMzlZ39OY~I$C*$j&8@?3XN7j2Fi`v0)%_4T7J??Qjgo?e!iu*L ztx~JNA3J{7_sMecsQ8_DgsdRJT!ttPk&`0Xk9iprm$0uf|HFhck?wGr!1{WdKznPT zVrVSk>~<~tYOBbZ+C>D87gNAMB-Aw-SA2NIO1d zy6C(#Dk7yNM?d(U>E}5Q8}%`L=AMgRb0&yIG~<_$9(-6y8cce6yJ6N&prTLiiO;RE zT5EDNnjHF?8-ItfJSQQ%1k*zc23K8Q3(=*Ct_Bqs=1H;Ol~*C0g>K z=w-Ep+bpxE{PgRzh)1)@^HG}w8MXT#a$%g$~rFWdOwvgKztmU)NBPOh)7 zzmMz`Hwk@<26|cZ!FP_-)g5`~AXyOj?K7f1cId>(GvY^wuf6dWI_96jYj9x9GA72Y z5YTy^p^il{(-2A5P^(o4c7u&`h<_3fvm55{3wQqQbNXBr?0=>3GLy<=x-GDPJb{5A zJha^e1}y0+jGE`^D$>^C^--2cAt(+l+6iPrG5|6d!C5McX!*+NWb%%)AxoKHtG29O zDSlD6=lL}&POmPqwyvobzhSrjoP1w2FtAI^6+lby;tHHe0f5ev2*Lrz59sShYMZx6x*Na&9U0h&aeDg?F&%DYwRYk{G z!N~2)@@=Chmzb+AEIzn_OqJ80^27>T4>)}SzxC4LB zD%RgSIQY|zmH$j~HfCoxI+9lPXEr#K6XUbW&Fu?f@wd4wGmf_|IhB+9==#xjOfpFh zf{$;27(0Ew){HBrjA3QGPG=|dT0q9>*h$00-7AWN%{v#3-MZ9SzU7gLRgZ5ipZ}=% z?=c0Mw?ZOtK@1yLF`09{#;SP>Zh;eCfPl4 zu}_G9B`ZEAE5yIinZ1=H@h#j#kS;ls>P|*Ga5b2I29m|KV-l&9a+0?ZDH%M;%YcNo zOgC{4(;xQD&%Db%#dCKs*YO7b_wxkbAFUutqA)m(cW^tWH?hUVMO+yP7mt1l=Op5V zm>7Kf?O4|)*kvPhq|0qbD;#3d3G8s5;dm)zqkCMkPl3RNVO);P3_JqO51$&Fh6*t? zR8o{C*IwH4)P<&3XR846Kl1pztkeo;>cAp>gB_!Fk0H6Twjr&jEgC{Sa>Gzg?%>hB>dE0AOHyWbzDfLz zHlIF`uT628)M^u83zjil{3m}xxRa@8x|!YX*jN(G&Tok0xZog+)ky@#MPz~iHzW~m zNO}-~pN9YyP9V?Adl(^oHVw6(^jwC5IeyOcqp`P~~Dng)|Ctb9&PNW3LQ!CMyW?;g2pbr~;Y4c;)2 zTvnQ|PKhtCt4lrl_Wn{q=soqx-nBp9QReNF<>bWXs!7_M$c|plx#+25J+^FXo@4k> z#|UER%p~t$6H_92N8hgI4UepG_1yX4uHCQiDhrwu6SgNhAv(D4;fuH3N>u+b)pgfr z;@?)UiPLj(jy%tuz*Hqb2mcB>SPm;}aHodQ02x*h>X~N-Bt=Yt6<{9Jua?{&qUE&2 zMiny0HH-E0#dpaE7s=b=yJ8{9JWjI3UkhiyZ()XYzi%n#$__O-*w>>HpGaI zG@}8p&b&0EZaev6>ZSelWa(Jxg5t!Q-7l>B*Y~T}ljW<$?|(D7g>0EZ@3IF@jEvlA zj&yg7Iv3wQ?qIJG`^k##p$YO!(zkG=i(tPs!!w?ecHoH56*f>?!m$E2A)HJBn~>uI zRs~KX#StXe`2XQP>MFY<@BsEIHfy?G4zE&@J|GMMy{pz{Lc)NqD5=qLIQg~mxqF{o zwf*(IH4O(|+qCYPoed%4c|N(YtHje+nzVk$@@;bR`tq}z%6m_Ld-&xe-=68M*z))) z&wxh@uVU!ccix)Zf%V>u^;m=T2uGZm;Z7IeGt8cUC@8wv5$IinCL&%j2RDXkse$y* zy<{OV?bP3qjKOBI{>WGN_VnES$7{#On>Tl4TDx}47tRho^uNc}9s9>a1N^V1RYj}r z8rv{%#~L@a`p3|NG4vpmNq48gL^Igz{n*wNcH$rgeh+|pe3H!hLOF#L%+Y|3=e+l` z&rDmm-+F7<`Ro>P_*3ER=i(9C_Hnem3~dK9I(Ld+ilMV5ZB2cOA4)RyBBeHyy@$m= za_@S}*a^1Bdx|!=2JLmgvlxecwNS6o2jdfMM0r7rb=t8 zu^4>HV%FNyV2G|Q!g^{}DFn!mYZh(4eZi*J4>iuc?iVXJKCz`bRQx7l>4alJg^BFm zuXVdDq2#Y@@cmjS>CSup`jc0G^4FhsmQ4P1@x)5!l3PY!|J8dWBuX7lf__WqVk72) zdRL*0)}4g8pzc6FUGJiLL_0L1xir5Bl4kz!72=oT5VXIVoFpoC%DYcE>pji3(>8yB zHbbR0F~9IM0?sm`CV*_1hGK43tJGJf^GgHpUvPEeXCz_zaiSEz63%`)t^DK@?kn2n z3e0f>+Eg;s&a%)MEJq_8ESG;;5frD!82olN+BcUoPB)iJ;s|Nn{n*&@N4M7?6elA~ zvs+xr!rAFLW2ZM&xK{jh<>X?6yUT{}+K&F?+nF#19%l&5DI}tf#W;%UVcqGRL`5s2 zB!e!OX2c0SN3^W{Ch?CXekU0qouZ0Nix0E&?-N@wFT)R$FT9(k-$vUf&~`W47MM7< zD&WGB?tsiiQa{!p%?LTm-alPL;y;Dp(izx_C;RYg1dT*PsA802K3rkR1pItD7k({_ zd5K^mqR~ki4ebz)pqtU0j*G@C`S1O=kcFZc_WZS{!$c9?`+(Jb-$EW_@AK|u7kXET zbMY-c$tcHf!Az1{BL^@Hl~0kpqIxW-Gzp3*X|}amLT7BX$YJU8yI3HOv60>gC8lK+h=+0p~j4Puuy8bqCIE+4A!4s;b>D zZP|M6K%Mw$Vo85x#R6AstZPAKMSpP;yX?T%_xJXm{_4Qn2fjYt*LV8s18moc@#5m~ z6J7t+d184<$+8o0(IkyjWL02Bar4U#RQNtX~+ztiG#L_V8XQu8Aa&mz^SpJ z5rHuf5b1Q}0r5}dZ&NSqsjc1f!qj{36?GOSCKh%Uy~nD&UkYa{H=bEMaL@XR&-5j8 zGi;q@X>@+t#f$teFh6lj7VQI62Yksoy0FzhHdiJ|24U#ba-<=o5dGgx*iO>Z`Paw9 zuXMG>Eidn>s@n7NmaT6bsHf95P*u5*PTRuDs)3S3*0cN1ZNI;RFu@`Hr{0~KKKSP| z{rzYDe30$FVR>=MvKzX^iXEfw(y<*C==ToDSHox zk{djew~+kZfL%odlc^eVhY)0tSHP;#y2fL+PmPylKir%y9aXS z6>2qr=+evV8PSPttB-Z}9UryFq|MGLRumwA?2`wn8uBZ4N6dKkh;FUncm5frSGzfG) zq60y0v^H7-qu!-o%Y zuTYHzz?y#m-zQ^~?kL*hS+;d1UWeWsCeg$pR*RpAHN-(~;N!k;!Bd0*{KG$i-@=#_ zcVdWu7DyesP1~W`7^9A&i9Cw7LHR(v;eX^TY+U^k!oDHy-zB~d;C9nFwwzn`eG9i> z`fl{~C)_CQTN2uS4Q+=~T@);X4MF2nyI*QMD$185l$1O4JN{=U5MtCJqjtCWr1%f< zf5k`f+bF5w?qNf`nch@3%KHub2UfDIT71Ln_=X@R!5v2tZhXP)NkH>~N#L{`+FnDF ziCuIM@jMZWNgk(QZ=0?WgW&(3p*;q`$Ztn`!PFxPCM+Ceih^MQ1A>6NX#RjoLzF_2 z25L#{6cbTHz5n*YyX=czHy=Yj{qzi<{5iBTA&13>xMtZ+kSc0dIef&y&w`qluxT2< zl3DR95~b8wbItePFM7!H!b|^pEHn6eVR^I^&u>uPG8;XO#OcC?mgzeUiX z6{m%>|N5Q(ds}9{7s3o}*N-nFY&+F<7zaxzN+YQIVdq5gliglgBa_CbzJUnH-#wV|Es zFB*A0y8Lgjd4X;#{qzKP_`Ub=JvZ|Ya$AH~@r1ShY$oE*P&_Uz0EVq`N21Zo0RFB= z{SdznG0~Ry#eb4q{z0OK23?JPS%x% z$^?)gN|$B~p>Q@*KYsr-&tbDkaKS8CTY)^AQo@DolFJ&S;&MVF1a(nur>*tcA=gFk z%_Dv7P2!iullLtfxNlRHO6mQ;TvK2UJLFkVkkA~Hml&6!jn1AwRdxCqcT-Poapk5( z&g@0kEfTXkR;4Rmib+XVJ{@N*HMnSk_84Yt0rF-_1VlD#?Dd3QF zWSUVpxE?Rd=aF?!YmEMnm~00 zUJ?Re;mgrW|Bxt%$NJ%2un>^K|3B`lVX^elgh@?F(5RJRAu@rn5R2T8^}&nw1D*lz zLay~A9f&)VSav+U&XvppF0b8k@4_uV-|BI1dv5d4{TnM-@q0GG(O@l`Z&SwDI^KxQ zC`oHOJXY9#{cEd|R=sxp{Gw$y_nB()k{#V^N|H(^yByRLn!xJ1{Hr_Qo&zyuu&}%k z?$0s%RuPHPnT5eBUY1-25_kDknp_SnIk5W6R?(NgSE;dvIzO%x;3hZ~{t#>;fB7xL z?b~yS`a*c zxg`$rxU<;SHAQ_TIk6p0l8F3rS?N~PaBp|BH2;d?D;7VJ%%e|5CE>A(vsC!g3_=1mRC zX&5L-pIhgSy*9pe<^GjfO@*cq@;VD_K4#v*=a-o_y?On-#sjZRt#$U4=_8XflTR3` zovGY*ZOMGQDx)c13t8L(TXYR_gBr%^&YOc#L?XvTZ4q2nmK%gLLK9MyptPQPrhwd_ zUo?zH5p6Um6hX+&NKQ&XoYZn(+D}OXA23!B0wRa7Lgy(dO^r(H^IWr*>=rk0%ib(p zdiO-W5ELT&eNfo)(ZV;U0TZyyN^w44jafB7e;)U3ay584OcuBRvP8t=DxgV9bWDI& zpjXIMuK2Y>3J^k{I*>GjFI`60LfTIr%3RPxxJoG*MA8XAa$A=>#E@<<8lnN)AR#2d zsGuU)6n?kL==5bc?S8-ApIVy{xRu4L?i}gen;q({-}Bt6oQdTFy3O|8O_MuvcFZ5X zV^uLxYh3dSYaxBT)rIr(6MWL=E*!n^#qu+2i(||l>wQI~n^%`OiUN}8$|Y~uoRTCG z<{(yZKK7Veigyt0M8ouA1$iF3%I_0a2jWRZv_Y9FxmkWChpiG&b67}nm*eKX_Cq82 z;+KhzhU}_Qt@tHTai^NQZR7V%c~`PW3fl7Ga;?+U_|Yo*p6tM?jiKe47s`@B!0E#Ykb#J8UB{Odle7;;%@G+FYow>*%P{ z7n;?4!Sqi*`GlP$|4eZjRjnRRtI7!G1l8nb)8!7w=+umwj!nXHW=1u82aByzaGSbk`mc z`}i)Y=SBve0bD+Imdh9L?*gs>CN$uYq44XFbY ze}#Y7@pIPySHcb)^RLDoV~TMc42({!g|R!h+g}~v9=)_Mk6XJJ<=qEEiEgh(De+0&c z5wWafu&6h5X)dtB{?P~SxNOX`OcgLLAErRRwj3^PfsOns*J$k2DI;F+CiGJ0($teb4|%AQ__| zzzIN4rP(^k*^rQ1EGl=Rbg4GoxjJw1pbksOg_k|aHz`8IufoH-+q`S}f`=9?#y;PG zJ=2c9I++3Aj)50&IYD;DXa@Xn_?eQk@PFJUQ60k^dD&T(^wb{#qiAwgViH`!qF5gm z<>0E-g#-1yw@j3}*PR((`RL{{@yn61{=pbqXT`uzH&j+`eRRc!XSR62XET?zTSv!9 zdR&?|AtE||?vl##4GSFhh1=>&+pBD`=~_)lWN!1yrkeGgIquONWf`T$s4|Rw3&!3g zlmpKhbcf7AU>*r)kM)zk65-%6vQ^aCW4fs0hlPNq_frUkK~EXm&_z9l<9uY zkVwfEKfff9LA5J%007u)$@Sv6Cbv2xrM%c4tx0U^?9J>tGV1#7JE2_s?oBbPusJJ6 z791ixuZW8b&Kms5Qu6hi1gv-F1jbMg?WbmXAB)Bi0^GGIs%fPz`(%d(fj$=N53e8} z2TAka&hv;T{3}iqbzM))_(%r-rh(Fs2H|khz=A7v-n^jgz>t%uqRkaKtz#OZp1yJJ zg1nUvZe+K4x0H1|V@sXvcNbozu^y;t{4Vr1h+=(oW_{3+B;yj;7cD_YY9|F&h?eb& zi1>!2io@asagd}EPg6ZRec@F!fVL=Zcq@Lr0!1sm3W{d)p=dUWqIniweDUvx_Wk`u zanatVNh|xO3$LHhFao zd;Wqxy>Fh26Xe3%oGIJ1g%6`6T#LC|fj;~Q=7w(It{=nPl;m&h=4m;*$a|Li?S~(p zVb6SWFM4bjZ{*&R6@z02oEj5CJvk2fLJkBiq;vptBHhg}63L!=0|L`*PFKW1pk);3 zq$o5+U+e(O5kK)Zb()m^m550+8h3JM%EsG^)1xkq$+YSLqf=T0mE<@e*9n$4O&rUexktNAE9xz7fvo0 zlFEuxt5&oXr71RU+qN;rRMIwH_J(gP1!LXSak)>g@r~vdQ$^3%^1QqDV9>khpph=6 zD;!TY@as4iqoY|h9cAl8N1@M%gdk^J2kk#l$v_bR$29BH(=JO<8T|->(I4UkV4ro? zxvMHN8tZD(a=NoLxkmM)8FN=wbR4V8>9T6F^(yw;x%&rQk=k6HDMMo|NLJ0=)9QkM4WL$j!IPYtC@O{wSvYeUaL(fyx`7GwkwFJ2BUaY%J@!XRJ^T<(x?D4JhGc*dC0Qni}+@GxLYIV9&YY{_2`Lg4^k z(Z~<*rCkAh$u+pA@%6zILpIyciNV+B?(Mf<4PmO=He4Ql{)?!-Lyrz84L^FQKkDNT z!z-5_X!`-uL{VafKaQLc{9tz>Ba`!}_?QSHd`K`N7M|+|g-9Y!RY=M3z)r_(TNsN{ zA7fCc6pENo$_i408A!t-TM1`3!dCgj0gkcHU&IzwYn5!aEHRb6^}@$pv55*nxbtRt z*}8j&mfo|nQ2d|7T#sH~VvP?b4z~8Dl9BXJK9Lz6rAhaG@a5=TD+&w8Z(L}r&WP6J zHQT=YfcnC%7eD7W^XZUW_$2O>_*i6Pkw(zW35G674U%yX=DIiqOKNm-lOdTX{JBtUT6Q8 zuI_x%$sd7aRx=AItE9ay4f!4^u^QNL;I6VWm@hbf{)MY$8VX!_Hk#N3Wfh6M%PFe{ zpHD(pNGj8E%p$N}l2zOUPt!nYZq%fxK|le63eWII+M3ELYIi?3IsDR377|rveQ);a z^$C@$J0{L<^t3mXRo3o#esbLlyK2NQl8U+tOS%g*ab>H!S3SI;)ccF3vJ4Y_XRJxJ zWF}UvA@48RW69k&)O~GlPI~_x>qudJhN7ihn`gDeB~=gX>{+lc!?tf|{=q?qYuW9i zmO5ujoY|F}=CGP#>GghPtg`$wHN zrU#(~Q-R6}lgX#R{kkv^xn5CA5gen>O-jnu#{^!bFYG!+vKK|zRbWU}BMFc>R? zAvrZwB}H^Uq9I*+YE*h^D0K&X_?I68Rw;Ep8m9!y({#FTxnWc#xDJr;4+hOG+Owvp zze2CC=r5YwGg(otcNn6g3=VyHx%Zcq>+V?4bNjl=%5}H*EVyG`Wl!6Y7nd)8@o-z) zVfuQcjXXWI#@w*P)9qf`Xqj4DQ<{)lncicr%tqNA*4Kh#npPg|tdmhUr@}NM>l?Am7M2}}ts=@>Z3BiL*PzaIpLGu19 z;U)=&`v3I=1*qIHCX>6wl( zNa^%Rd_eLKxAkCe^z>j)1YSww%HOTnRaJI$&E(W7l{qE4pmu;wobt*1lwanj22XFP zso8RR5U^}eb!7;`BI4(_mww|{k;%& zH+;ljEwq0SN=eq*<3e}ud!;QqqLbCO_nY?T9-ww7iDi6v8zIv zIW)fjPzEX(t~B^S3@aB{>R8FTuw9WE@|)sFT1qPw<4FPqqX@BL|C`$0e~4#D+dqh9 z`fhFkcj~-&h^#tKdcU9k6eBdVH*@z0&w!zP;xVWUSV*5Z$75!ERa(%hJhKqDN}e;y z+@Ko5An-qS2TTO+i1;^m&^G?v<6VFQV)ao*KM~qasO^*x9G#?oAz#qMaxHg{_XJ7K z&&kQocbzl0OjIAcL*r<1Op-zN=G}zLZI3l(ms>I2mhEdZYN~213#oJ%*y}Ljr?$hhK=ZDRuPyC#Y=qe%<$_m^FEAOjzO5OxIIua_JhahmI~Da zp3+CrdNHh&MAo8ocNvUMph)1d>KL0uNv0#$v0P`7D=aQODWm9I&b;xWsx|X6Yf7t| zbGz16xmI+!-tgoXJ1UAh`4u_oa}v}MsY%YZ{KUlkc4zT?b#zA~QeDaMg$v75Z4SL% z+9O-QzF2xW`vU5Vrfa0tK~ey3!A6xf4?`Mf*cV!&WAjO__|?0@tKt_>ZD)YH4kE@^ zF$>+H!I-^h6w^u*N{jttQR^uIL=bC#Av54p50A-92wakq%b<=(h07FO#bqCLTg9lN zKo&4a)a_YtkXFhw8vzWF7d1H*>2rqx2Z>&pWo6CMTJcLjEZ4UT<;UdPICA0D8f$j8 zSSkD-eSH_-@)u}^2x+HdaJ1@@a_gjA2}TRrr%B6$nFz><=7#CYXce5gb?V8@ZujOV zrvSACRc$^qJalGr_4h4wmcC(4oSAEd{-9N@hAI%I+{Jkrdv*~ zqrPMa`!M%B^efGlfXM~z2FRv&M$K~cEP%@$%7Asb+BpCr+1=Y(=5FiG&hFaQ(z3lf z=Ru{#lVNe2m3XyS+!iH2S-Y_-H@9nJEq>|D&F$P+n_gi{NU&9;Pns%n6BBcRT)^Ic z%53E`IG|-@^5OudM@}Z0_ZfP{1C4i7c!bF!k6$G0U-EBa(#YC~ATozMMamck?-kT1 zVE^Lt3C;%j>ZBNyVt{Y=D2uz#1<|L)ciC_AZ^CoO$%>ihq}!=0!G7z1jz&5!3QyAK z2IzB7kyZ3=Jon^9C41igeZJ>7Ja?Wxhk1NMoI43)q2B0zWKvqKX$~O}+{}r#8T70`5@4Ik_1< z=A*lCY9ZE^o#M&U∾%W+xJ&W~T?p}t*J3F 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

x<71uZaKL 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 Z{5zjejQ_)(PbtcOweyBzqHlNp^k2J#@uUC% literal 0 HcmV?d00001 diff --git a/base/src/main/res/font/mono_bold_xml.xml b/base/src/main/res/font/mono_bold_xml.xml new file mode 100644 index 00000000..7758021d --- /dev/null +++ b/base/src/main/res/font/mono_bold_xml.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/font/mono_medium.ttf b/base/src/main/res/font/mono_medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c496725e61bdf656da60056e77e096ceff69a5e0 GIT binary patch literal 109292 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 RD9V3#^gYEye>nR0e*yc@D3|~M literal 0 HcmV?d00001 diff --git a/base/src/main/res/font/mono_medium_xml.xml b/base/src/main/res/font/mono_medium_xml.xml new file mode 100644 index 00000000..73d536be --- /dev/null +++ b/base/src/main/res/font/mono_medium_xml.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/font/mono_regular.ttf b/base/src/main/res/font/mono_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5919b5d1bf061d687f60300fd7ab774c6b06df50 GIT binary patch literal 109212 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 Qit=CXyrr1vyPZG%7iFl@s{jB1 literal 0 HcmV?d00001 diff --git a/base/src/main/res/font/mono_regular_xml.xml b/base/src/main/res/font/mono_regular_xml.xml new file mode 100644 index 00000000..4eb647d8 --- /dev/null +++ b/base/src/main/res/font/mono_regular_xml.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/font/serif_bold.ttf b/base/src/main/res/font/serif_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2726b0a0005b6b6041d7f530de3e755bfc4c7dad GIT binary patch literal 348704 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%0D3eK5AZ>!Bi}&SYizV0#^3CD%B7c$SSP`@QC4eH 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

+$4qsBLWepQzo&)GQs(9DV@LVKuCZIk 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 + + + \ No newline at end of file diff --git a/base/src/main/res/font/serif_regular.ttf b/base/src/main/res/font/serif_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0e8752414cc1da8624870a6f66d4b1b759aabaad GIT binary patch literal 330000 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�D0MqIcDy^Y9RribJu9V4tkr0j;!%+*B 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

Y$6NRJ|=lWICl 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 + + + \ No newline at end of file diff --git a/base/src/main/res/layout/item_format_code.xml b/base/src/main/res/layout/item_format_code.xml index 6add9912..8add1520 100644 --- a/base/src/main/res/layout/item_format_code.xml +++ b/base/src/main/res/layout/item_format_code.xml @@ -8,7 +8,6 @@ android:id="@+id/text" style="@style/FormatText" android:padding="@dimen/spacing_normal" - android:fontFamily="monospace" android:textColor="@color/transparent" android:textColorHint="@color/transparent" /> @@ -16,7 +15,6 @@ android:id="@+id/edit" style="@style/FormatText" android:padding="@dimen/spacing_normal" - android:fontFamily="monospace" android:textColor="@color/transparent" android:textColorHint="@color/transparent" /> \ No newline at end of file diff --git a/base/src/main/res/layout/item_format_heading.xml b/base/src/main/res/layout/item_format_heading.xml index ef2aaa26..d4336d4a 100644 --- a/base/src/main/res/layout/item_format_heading.xml +++ b/base/src/main/res/layout/item_format_heading.xml @@ -7,12 +7,10 @@ \ No newline at end of file diff --git a/base/src/main/res/layout/item_import_file.xml b/base/src/main/res/layout/item_import_file.xml index 3bb00990..014f8e2e 100644 --- a/base/src/main/res/layout/item_import_file.xml +++ b/base/src/main/res/layout/item_import_file.xml @@ -52,7 +52,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:fontFamily="@style/FontText" android:text="12 Aug 2017, 11:00 PM" android:textColor="@color/dark_hint_text" android:textSize="@dimen/font_size_small" /> @@ -61,7 +60,6 @@ android:id="@+id/file_size" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:fontFamily="@style/FontText" android:text="2.0MB" android:textColor="@color/dark_hint_text" android:textSize="@dimen/font_size_small" /> diff --git a/base/src/main/res/layout/layout_choose_sheet_item.xml b/base/src/main/res/layout/layout_choose_sheet_item.xml index f0939075..4e865c2f 100644 --- a/base/src/main/res/layout/layout_choose_sheet_item.xml +++ b/base/src/main/res/layout/layout_choose_sheet_item.xml @@ -11,5 +11,4 @@ ui:iconTextGap="@dimen/spacing_normal" ui:iconTint="@color/dark_tertiary_text" ui:textColor="@color/dark_secondary_text" - ui:textSize="@dimen/font_size_normal" - ui:textStyle="@style/FontText"/> \ No newline at end of file + ui:textSize="@dimen/font_size_normal"/> \ No newline at end of file diff --git a/base/src/main/res/layout/layout_home_tag_item.xml b/base/src/main/res/layout/layout_home_tag_item.xml index 14709a8f..57b58961 100644 --- a/base/src/main/res/layout/layout_home_tag_item.xml +++ b/base/src/main/res/layout/layout_home_tag_item.xml @@ -20,7 +20,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - style="@style/FontText" android:textColor="@color/dark_secondary_text" android:textSize="@dimen/font_size_normal" /> diff --git a/base/src/main/res/layout/layout_option_sheet_item.xml b/base/src/main/res/layout/layout_option_sheet_item.xml index 79821784..dba77624 100644 --- a/base/src/main/res/layout/layout_option_sheet_item.xml +++ b/base/src/main/res/layout/layout_option_sheet_item.xml @@ -12,9 +12,7 @@ ui:iconTint="@color/dark_tertiary_text" ui:subtitleColor="@color/dark_tertiary_text" ui:subtitleSize="@dimen/font_size_small" - ui:subtitleStyle="@style/FontText" ui:titleColor="@color/dark_secondary_text" ui:titleSize="@dimen/font_size_normal" - ui:titleStyle="@style/FontText" ui:actionSize="@dimen/icon_size_normal" ui:actionTint="@color/material_blue_400"/> \ No newline at end of file diff --git a/base/src/main/res/layout/toolbar_folder_bottom.xml b/base/src/main/res/layout/toolbar_folder_bottom.xml index 7074e808..bcfd6f2f 100644 --- a/base/src/main/res/layout/toolbar_folder_bottom.xml +++ b/base/src/main/res/layout/toolbar_folder_bottom.xml @@ -20,7 +20,6 @@ @style/RobotoTextViewStyle @style/RobotoButtonStyle @style/RobotoEditTextStyle - monospace @color/white true diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index defbbcca..4a9099c5 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -495,4 +495,16 @@ Remove Notes Keep Notebook but all notes move to trash or are deleted. + + Fonts + Select the app and note fonts + Select Font + Note: Fonts might not work properly with all languages + + App Default + Device Default + Mono + Serif + + diff --git a/base/src/main/res/values/styles.xml b/base/src/main/res/values/styles.xml index 09ae99f8..d6269841 100644 --- a/base/src/main/res/values/styles.xml +++ b/base/src/main/res/values/styles.xml @@ -8,7 +8,6 @@ @style/RobotoTextViewStyle @style/RobotoButtonStyle @style/RobotoEditTextStyle - @font/open_sans - - - - - - -