diff --git a/app/build.gradle b/app/build.gradle index 01dfac23..26033494 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "com.mickstarify.zooforzotero" minSdkVersion 21 targetSdkVersion 29 - versionCode 34 - versionName "2.4a" + versionCode 35 + versionName "2.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "zotero_api_key", apikeyProperties['zotero_api_key']) buildConfigField("String", "zotero_api_secret", apikeyProperties['zotero_api_secret']) @@ -55,15 +55,15 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.1.0' + implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation "androidx.navigation:navigation-fragment-ktx:2.1.0" - implementation "androidx.navigation:navigation-ui-ktx:2.1.0" - implementation 'com.google.android.material:material:1.0.0' - implementation 'com.google.android.material:material:1.2.0-alpha03' + implementation "androidx.navigation:navigation-fragment-ktx:2.3.0" + implementation "androidx.navigation:navigation-ui-ktx:2.3.0" + implementation 'com.google.android.material:material:1.1.0' + implementation 'com.google.android.material:material:1.3.0-alpha01' api 'com.google.guava:guava:28.1-jre' implementation 'oauth.signpost:oauth-signpost:1.2.1.2' - implementation("com.squareup.okhttp3:okhttp:4.2.1") + implementation("com.squareup.okhttp3:okhttp:4.2.2") implementation('com.squareup.okhttp3:logging-interceptor:4.2.2') implementation('oauth.signpost:signpost-commonshttp4:1.2.1.2') { exclude group: 'org.apache.httpcomponents' @@ -76,14 +76,14 @@ dependencies { implementation 'org.jetbrains.anko:anko:0.10.8' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.preference:preference:1.1.1' testImplementation 'junit:junit:4.12' testImplementation "org.mockito:mockito-core:2.28.2" androidTestImplementation "org.mockito:mockito-core:2.28.2" androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation 'com.google.firebase:firebase-analytics:17.2.2' + implementation 'com.google.firebase:firebase-analytics:17.4.3' implementation 'net.lingala.zip4j:zip4j:2.2.7' releaseImplementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' @@ -96,9 +96,9 @@ dependencies { debugImplementation 'com.facebook.soloader:soloader:0.8.0' // Room Dependencies - implementation "androidx.room:room-runtime:2.2.3" - implementation 'androidx.room:room-rxjava2:2.2.3' - kapt "androidx.room:room-compiler:2.2.3" + implementation "androidx.room:room-runtime:2.2.5" + implementation 'androidx.room:room-rxjava2:2.2.5' + kapt "androidx.room:room-compiler:2.2.5" // Dagger Dependencies implementation 'com.google.dagger:dagger:2.25.2' diff --git a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/Contract.kt b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/Contract.kt index 5dfcfa4e..f8cf1cda 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/Contract.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/Contract.kt @@ -72,8 +72,8 @@ interface Contract { fun refreshItemView() fun displayGroupsOnActionBar(groups: List) fun openGroup(groupTitle: String) - fun startUploadingAttachment(attachment: Item) - fun stopUploadingAttachment() + fun startUploadingAttachmentProgress(attachment: Item) + fun stopUploadingAttachmentProgress() fun onResume() fun createYesNoPrompt( title: String, message: String, yesText: String, noText: String, onYesClick: () -> Unit, @@ -83,6 +83,8 @@ interface Contract { fun showBasicSyncAnimation() fun hideBasicSyncAnimation() fun openTrash() + fun uploadAttachment(item: Item) + fun requestForceResync() } interface Model { diff --git a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemAttachmentEntry.kt b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemAttachmentEntry.kt index cfc067c4..59a181f3 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemAttachmentEntry.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemAttachmentEntry.kt @@ -3,6 +3,7 @@ package com.mickstarify.zooforzotero.LibraryActivity.ItemView import android.app.AlertDialog import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle @@ -15,7 +16,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment import com.mickstarify.zooforzotero.R import com.mickstarify.zooforzotero.ZoteroStorage.Database.Item -import org.jetbrains.anko.support.v4.toast +import org.jetbrains.anko.sdk27.coroutines.onLongClick private const val ARG_ATTACHMENT = "attachment" @@ -44,18 +45,19 @@ class ItemAttachmentEntry : Fragment() { filename.text = attachment?.data?.get("filename") ?: "unknown" if (linkMode == "linked_file") { // this variant uses title as a filename. filename.text = "[Linked] ${attachment?.getItemData("title")}" - }else if (linkMode == "linked_url") { + + } else if (linkMode == "linked_url") { filename.text = "[Linked Url] ${attachment?.getItemData("title")}" layout.setOnClickListener { val url = attachment?.getItemData("url") AlertDialog.Builder(context) .setMessage("Would you like to open this URL: $url") - .setPositiveButton("Yes", {dialog, which -> + .setPositiveButton("Yes", { dialog, which -> val intent = Intent(Intent.ACTION_VIEW) intent.setData(Uri.parse(url)) startActivity(intent) }) - .setNegativeButton("No", {_,_ -> }) + .setNegativeButton("No", { _, _ -> }) .show() } } @@ -69,16 +71,40 @@ class ItemAttachmentEntry : Fragment() { icon.setImageResource(R.drawable.djvu_icon) } else if (attachment?.getFileExtension() == "epub") { icon.setImageResource(R.drawable.epub_icon) + } else { + // todo get default attachment icon. } layout.setOnClickListener { if (linkMode == "linked_file") { - toast("This attachment is linked and I cannot download it.") + fileOpenListener?.openLinkedAttachmentListener( + attachment ?: throw Exception("No Attachment given.") + ) } else { fileOpenListener?.openAttachmentFileListener( attachment ?: throw Exception("No Attachment given.") ) } } + + layout.onLongClick { + AlertDialog.Builder(context) + .setTitle("Attachment") + .setItems( + arrayOf("Open", "Force Re-upload"), + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface?, item: Int) { + when (item) { + 0 -> fileOpenListener?.openAttachmentFileListener( + attachment ?: throw Exception("No Attachment given.") + ) + 1 -> fileOpenListener?.forceUploadAttachmentListener( + attachment ?: throw Exception("No Attachment given.") + ) + } + } + + }).show() + } } return view } @@ -94,6 +120,8 @@ class ItemAttachmentEntry : Fragment() { interface OnAttachmentFragmentInteractionListener { fun openAttachmentFileListener(item: Item) + fun forceUploadAttachmentListener(item: Item) + fun openLinkedAttachmentListener(item: Item) } companion object { diff --git a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemViewFragment.kt b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemViewFragment.kt index afd7f91f..9fbd94c6 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemViewFragment.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/ItemView/ItemViewFragment.kt @@ -117,10 +117,10 @@ class ItemViewFragment : BottomSheetDialogFragment(), NoteInteractionListener { addTextEntry("Item Type", item.data["itemType"] ?: "Unknown") addTextEntry("title", item.getTitle()) if (item.creators.isNotEmpty()) { - this.addCreators(item.creators) + this.addCreators(item.getSortedCreators()) } else { // empty creator. - this.addCreators(listOf(Creator("null", "", "", ""))) + this.addCreators(listOf(Creator("null", "", "", "", -1))) } for ((key, value) in item.data) { if (value != "" && key != "itemType" && key != "title") { diff --git a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivity.kt b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivity.kt index e084adcf..ebd0a36a 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivity.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivity.kt @@ -73,6 +73,7 @@ class LibraryActivity : AppCompatActivity(), Contract.View, override fun initUI() { val navigationView = findViewById(R.id.nav_view_library) + navigationView.setCheckedItem(R.id.my_library) collectionsMenu = navigationView.menu.addSubMenu( R.id.group_collections, Menu.NONE, @@ -159,6 +160,9 @@ class LibraryActivity : AppCompatActivity(), Contract.View, val intent = Intent(this, AttachmentManager::class.java) startActivity(intent) } + R.id.force_resync -> { + presenter.requestForceResync() + } } return super.onOptionsItemSelected(item) @@ -461,6 +465,14 @@ class LibraryActivity : AppCompatActivity(), Contract.View, presenter.openAttachment(item) } + override fun forceUploadAttachmentListener(item: Item) { + presenter.uploadAttachment(item) + } + + override fun openLinkedAttachmentListener(item: Item) { + presenter.openAttachment(item) + } + override fun onListFragmentInteraction(item: Item?) { Log.d("zotero", "got onListFragmentInteraction from item ${item?.itemKey}") } diff --git a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityModel.kt b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityModel.kt index 76eb8ebe..2be9789d 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityModel.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityModel.kt @@ -550,6 +550,17 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex /* This is the point of entry when a user clicks an attachment on the UI. * We must decide whether we want to intitiate a download or just open a local copy. */ + // first check to see if we are opening a linked attachment + if (item.data["linkMode"] == "linked_file"){ + val intent = attachmentStorageManager.openLinkedAttachment(item) + if (intent != null){ + context.startActivity(intent) + } else { + presenter.makeToastAlert("Error, could not find linked attachment ${item.data["path"]}") + } + return + } + // check to see if the attachment exists but is invalid val attachmentExists: Boolean try { @@ -1034,7 +1045,13 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex } override fun uploadAttachment(attachment: Item) { - val md5Key = attachmentStorageManager.calculateMd5(attachment) + val md5Key: String + try { + md5Key = attachmentStorageManager.calculateMd5(attachment) + } catch (e: FileNotFoundException){ + presenter.makeToastAlert("Cannot upload attachment. File does not exist.") + return + } var mtime = attachmentStorageManager.getMtime(attachment) if (mtime < attachment.getMtime()) { @@ -1056,7 +1073,7 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe( object : CompletableObserver { override fun onComplete() { - presenter.stopUploadingAttachment() + presenter.stopUploadingAttachmentProgress() removeFromRecentlyViewed(attachment) zoteroDB.updateAttachmentMetadata( attachment.itemKey, @@ -1071,7 +1088,7 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex } override fun onSubscribe(d: Disposable) { - presenter.startUploadingAttachment(attachment) + presenter.startUploadingAttachmentProgress(attachment) } override fun onError(e: Throwable) { @@ -1086,16 +1103,18 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex "error_uploading_attachments_webdav", bundle ) - presenter.stopUploadingAttachment() + presenter.stopUploadingAttachmentProgress() } }) return } else { - zoteroAPI.updateAttachment(attachment).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()).subscribe(object : + zoteroAPI.updateAttachment(attachment) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : CompletableObserver { override fun onComplete() { - presenter.stopUploadingAttachment() + presenter.stopUploadingAttachmentProgress() removeFromRecentlyViewed(attachment) zoteroDB.updateAttachmentMetadata( attachment.itemKey, @@ -1106,11 +1125,11 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex } override fun onSubscribe(d: Disposable) { - presenter.startUploadingAttachment(attachment) + presenter.startUploadingAttachmentProgress(attachment) } override fun onError(e: Throwable) { - if (e is com.mickstarify.zooforzotero.ZoteroAPI.AlreadyUploadedException) { + if (e is AlreadyUploadedException) { removeFromRecentlyViewed(attachment) zoteroDB.updateAttachmentMetadata( attachment.itemKey, @@ -1118,6 +1137,7 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex attachmentStorageManager.getMtime(attachment), AttachmentInfo.WEBDAV ).subscribeOn(Schedulers.io()).subscribe() + presenter.makeToastAlert("Attachment already up to date.") } else if (e is PreconditionFailedException) { presenter.createErrorAlert( "Error uploading Attachment", @@ -1140,7 +1160,7 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex Bundle().apply { putString("error_message", e.toString()) } firebaseAnalytics.logEvent("error_uploading_attachments", bundle) } - presenter.stopUploadingAttachment() + presenter.stopUploadingAttachmentProgress() } }) @@ -1471,27 +1491,6 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex } checkAttachmentStorageAccess() - - /*Do to a change in implementation this code will transition webdav configs.*/ - if (preferences.firstRunForVersion25()) { - if (preferences.isWebDAVEnabled()) { - val address = preferences.getWebDAVAddress() - val newAddress = if (address.endsWith("/zotero")) { - address - } else { - if (address.endsWith("/")) { // so we don't get server.com//zotero - address + "zotero" - } else { - address + "/zotero" - } - } - preferences.setWebDAVAuthentication( - newAddress, - preferences.getWebDAVUsername(), - preferences.getWebDAVPassword() - ) - } - } } @@ -1502,4 +1501,9 @@ class LibraryActivityModel(private val presenter: Contract.Presenter, val contex fun getCollectionFromKey(collectionKey: String): Collection? { return zoteroDB.getCollectionById(collectionKey) } + + fun destroyLibrary() { + zoteroDB.clearItemsVersion() + zoteroDatabase.deleteEverything() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityPresenter.kt b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityPresenter.kt index 18770db3..c16d6338 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityPresenter.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/LibraryActivity/LibraryActivityPresenter.kt @@ -14,8 +14,16 @@ class LibraryActivityPresenter(val view: Contract.View, context: Context) : Cont when (model.preferences.getSortMethod()) { SortMethod.TITLE -> it.getTitle().toLowerCase(Locale.ROOT) SortMethod.DATE -> it.getSortableDateString() - SortMethod.AUTHOR -> it.getAuthor().toLowerCase(Locale.ROOT) SortMethod.DATE_ADDED -> it.getSortableDateAddedString() + SortMethod.AUTHOR -> { + val authorText = it.getAuthor().toLowerCase(Locale.ROOT) + // force empty authors to the bottom. Just like the zotero desktop client. + if (authorText == ""){ + "zzz" + } else { + authorText + } + } } }.thenBy { it.getTitle().toLowerCase(Locale.ROOT) } @@ -26,11 +34,11 @@ class LibraryActivityPresenter(val view: Contract.View, context: Context) : Cont } - override fun startUploadingAttachment(attachment: Item) { + override fun startUploadingAttachmentProgress(attachment: Item) { view.showAttachmentUploadProgress(attachment) } - override fun stopUploadingAttachment() { + override fun stopUploadingAttachmentProgress() { view.hideAttachmentUploadProgress() view.makeToastAlert("Finished uploading attachment.") } @@ -239,6 +247,14 @@ class LibraryActivityPresenter(val view: Contract.View, context: Context) : Cont view.populateEntries(entries) } + override fun uploadAttachment(item: Item) { + model.uploadAttachment(item) + } + + override fun requestForceResync() { + model.destroyLibrary() + } + override fun setCollection(collectionKey: String, isSubCollection: Boolean) { /*SetCollection is the method used to display items on the listView. It * has to get the data, then sort it, then provide it to the view.*/ @@ -319,18 +335,12 @@ class LibraryActivityPresenter(val view: Contract.View, context: Context) : Cont view.initUI() view.showLoadingAnimation(true) view.showLibraryContentDisplay("Loading your library content.") - - //TODO i will delete this code next version. (just for version 2.2) - if (model.preferences.firstRunForVersion27() && model.hasOldStorage()) { - model.migrateFromOldStorage() + if (model.shouldIUpdateLibrary()) { + model.loadGroups() + model.downloadLibrary() } else { - if (model.shouldIUpdateLibrary()) { - model.loadGroups() - model.downloadLibrary() - } else { - model.loadLibraryLocally() - model.loadGroups() - } + model.loadLibraryLocally() + model.loadGroups() } } diff --git a/app/src/main/java/com/mickstarify/zooforzotero/PreferenceManager.kt b/app/src/main/java/com/mickstarify/zooforzotero/PreferenceManager.kt index 9fb9cd5f..a070ea9f 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/PreferenceManager.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/PreferenceManager.kt @@ -176,30 +176,6 @@ class PreferenceManager @Inject constructor(context: Context) { return sharedPreferences.getBoolean("attachments_uploading_enabled", true) } - fun firstRunForVersion25(): Boolean { - /*check to see if this is the first time the user is opening on version 2.1c (25)*/ - val firstRun = sharedPreferences.getBoolean("firstrun_version25", true) - if (firstRun) { - val editor = sharedPreferences.edit() - editor.putBoolean("firstrun_version25", false) - editor.apply() - } - return firstRun - - } - - fun firstRunForVersion27(): Boolean { - /*check to see if this is the first time the user is opening on version 2.2 (27)*/ - val firstRun = sharedPreferences.getBoolean("firstrun_version27", true) - if (firstRun) { - val editor = sharedPreferences.edit() - editor.putBoolean("firstrun_version27", false) - editor.apply() - } - return firstRun - - } - fun shouldOpenPDFOnOpen(): Boolean { return sharedPreferences.getBoolean("should_open_pdf_on_open", false) diff --git a/app/src/main/java/com/mickstarify/zooforzotero/SyncSetup/SyncSetupView.kt b/app/src/main/java/com/mickstarify/zooforzotero/SyncSetup/SyncSetupView.kt index 3fc61a28..a2bf51ec 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/SyncSetup/SyncSetupView.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/SyncSetup/SyncSetupView.kt @@ -84,9 +84,6 @@ class SyncSetupView : AppCompatActivity(), SyncSetupContract.View { val rg_cloudproviders = findViewById(R.id.radiogroup_cloudproviders) rg_cloudproviders.setOnCheckedChangeListener { _, i -> when (i) { - R.id.radio_dropbox -> selected_provider = SyncOption.Dropbox - R.id.radio_googledrive -> selected_provider = SyncOption.GoogleDrive - R.id.radio_onedrive -> selected_provider = SyncOption.Onedrive R.id.radio_zotero -> selected_provider = SyncOption.ZoteroAPI R.id.radio_zotero_manual_apikey -> selected_provider = SyncOption.ZoteroAPIManual else -> throw Exception("Error, not sure what Radiobox was pressed") diff --git a/app/src/main/java/com/mickstarify/zooforzotero/ZooForZoteroApplication.kt b/app/src/main/java/com/mickstarify/zooforzotero/ZooForZoteroApplication.kt index 0e96aae2..c0343bad 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/ZooForZoteroApplication.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/ZooForZoteroApplication.kt @@ -1,12 +1,12 @@ package com.mickstarify.zooforzotero import android.app.Application -//import com.facebook.flipper.android.AndroidFlipperClient -//import com.facebook.flipper.android.utils.FlipperUtils -//import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin -//import com.facebook.flipper.plugins.inspector.DescriptorMapping -//import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin -//import com.facebook.soloader.SoLoader +import com.facebook.flipper.android.AndroidFlipperClient +import com.facebook.flipper.android.utils.FlipperUtils +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin +import com.facebook.flipper.plugins.inspector.DescriptorMapping +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin +import com.facebook.soloader.SoLoader import com.mickstarify.zooforzotero.di.component.ApplicationComponent import com.mickstarify.zooforzotero.di.component.DaggerApplicationComponent import com.mickstarify.zooforzotero.di.module.ApplicationModule @@ -18,14 +18,14 @@ class ZooForZoteroApplication : Application() { override fun onCreate() { super.onCreate() -// SoLoader.init(this, false) -// -// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) { -// val client = AndroidFlipperClient.getInstance(this) -// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults())) -// client.addPlugin(DatabasesFlipperPlugin(this)); -// client.start() -// } + SoLoader.init(this, false) + + if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) { + val client = AndroidFlipperClient.getInstance(this) + client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults())) + client.addPlugin(DatabasesFlipperPlugin(this)); + client.start() + } component = DaggerApplicationComponent.builder().applicationModule( diff --git a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroAPI/ZoteroAPI.kt b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroAPI/ZoteroAPI.kt index d31b6599..f090b818 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroAPI/ZoteroAPI.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroAPI/ZoteroAPI.kt @@ -563,7 +563,8 @@ class ZoteroAPI( val newMd5 = attachmentStorageManager.calculateMd5(attachment) if (oldMd5 == newMd5) { - throw AlreadyUploadedException("Local attachment version is the same as Zotero's.") + + return Completable.error(AlreadyUploadedException("Local attachment version is the same as Zotero's.")) } val mtime = attachmentStorageManager.getMtime(attachment) val filename = attachmentStorageManager.getFilenameForItem(attachment) diff --git a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/AttachmentStorageManager.kt b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/AttachmentStorageManager.kt index a458283c..f035c11b 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/AttachmentStorageManager.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/AttachmentStorageManager.kt @@ -240,6 +240,24 @@ class AttachmentStorageManager @Inject constructor( return intent } + fun openAttachment(uri: Uri, contentType:String = ""): Intent { + var intent = Intent(Intent.ACTION_VIEW) + Log.d("zotero", "opening PDF with Uri $uri") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intent = Intent(Intent.ACTION_VIEW) + intent.data = uri + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } else { + intent.setDataAndType(uri, contentType) + intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY + intent = Intent.createChooser(intent, "Open File") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + } + return intent + } + fun testStorage(): Boolean { if (storageMode == StorageMode.EXTERNAL_CACHE) { @@ -368,4 +386,74 @@ class AttachmentStorageManager @Inject constructor( } } + fun openLinkedAttachment(item: Item): Intent? { + /* This method searches for the linked attachment in the users storage directory. + * This may fail and return nothing. */ + if (item.data["linkMode"] != "linked_file") { + throw Exception("error attempting to open linked attachment on something this isn't elligible.") + } + + // will work as followed: + // /home/michael/Downloads/Alice in Wonderland.pdf -> [home, michael, Downloads. Alice in Wonderland.pdf] + // for each path in list, check if folder exists in root dir + // if it does, pursue this path. + // if it doesn't, return to the root path, and repeat for the next item in the list. + + + var itemPath = item.data["path"] ?: "" + // do some transformations for windows style paths + // e.g C:\\Users\\michael\\file.txt -> C:/Users/michael/file.txt + itemPath = itemPath.replace("\\", "/") + val directories = itemPath.split("/") + if (storageMode == StorageMode.CUSTOM) { + for (index in directories.indices) { + val location = preferenceManager.getCustomAttachmentStorageLocation() + var documentFile = DocumentFile.fromTreeUri(context, Uri.parse(location)) + var i = index + while (i < directories.size) { + documentFile = documentFile?.findFile(directories[i]) + Log.d("zotero", "checking ${directories[i]}") + if (documentFile?.exists() ?: false) { + i++ + } else { + break + } + } + if (documentFile?.isFile ?: false) { + Log.d("zotero", "found file ${documentFile?.name}") + return openAttachment(documentFile!!.uri, item.data["contentType"]?:"") + } + } + } else if (storageMode == StorageMode.EXTERNAL_CACHE) { + // logic using File api is much simpler + for (i in directories.indices){ + var path = directories.slice(i..directories.size-1).joinToString("/") + if (path.first() == '/'){ + path = path.slice(1..path.length-1) + } + Log.d("zotero", "checking path $path") + var document = File(context.externalCacheDir, path) + if (document.exists()){ + val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + document + ) + } else { + Uri.fromFile(document) + } + return openAttachment(uri, item.data["contentType"]?:"") + } + } + + + } + else { + throw NotImplementedError() + } + + return null + } + } \ No newline at end of file diff --git a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Database.kt b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Database.kt index 16a5c994..e975fdff 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Database.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Database.kt @@ -25,7 +25,7 @@ import javax.inject.Singleton ItemCollection::class, AttachmentInfo::class ), - version = 4, + version = 5, exportSchema = true ) abstract class ZoteroRoomDatabase : RoomDatabase() { @@ -44,7 +44,8 @@ class ZoteroDatabase @Inject constructor(val context: Context) { ) .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) - .addMigrations(MIGRATION_3_4).build() + .addMigrations(MIGRATION_3_4) + .addMigrations(MIGRATION_4_5).build() fun addGroup(group: GroupInfo): Completable { return db.groupInfoDao().insertGroupInfos(group) @@ -123,13 +124,14 @@ class ZoteroDatabase @Inject constructor(val context: Context) { } itemDatas.add(itemData) } - for (creatorPOJO in itemPOJO.creators) { + for ((index, creatorPOJO) in itemPOJO.creators.withIndex()) { itemCreators.add( Creator( itemPOJO.ItemKey, creatorPOJO.firstName, creatorPOJO.lastName, - creatorPOJO.creatorType + creatorPOJO.creatorType, + index ) ) } @@ -196,6 +198,9 @@ class ZoteroDatabase @Inject constructor(val context: Context) { //todo return Completable.complete() } + + fun deleteEverything() { + } } private fun Boolean.Companion.fromInt(param: Int): Boolean { diff --git a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Item.kt b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Item.kt index 17041430..4d2f2ec4 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Item.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Item.kt @@ -61,6 +61,10 @@ class Item : Parcelable { return itemInfo.groupParent } + fun getSortedCreators(): List { + return this.creators.sortedBy { it.order } + } + fun getVersion(): Int { return itemInfo.version } @@ -123,7 +127,7 @@ class Item : Parcelable { return when (creators.size) { 0 -> "" 1 -> creators[0].lastName - else -> "${creators[0].lastName} et al." + else -> "${getSortedCreators()[0].lastName} et al." } } @@ -186,12 +190,25 @@ class Item : Parcelable { } fun getFileExtension(): String { - return when (this.data["contentType"]) { + val extension = when (this.data["contentType"]) { "application/pdf" -> "pdf" "image/vnd.djvu" -> "djvu" "application/epub+zip" -> "epub" + "application/x-mobipocket-ebook" -> "mobi" + "application/vnd.amazon.ebook" -> "azw" else -> "UNKNOWN" } + + // I probably should have just used file extensions from the beginning... + if (extension == "UNKNOWN") { + val filename = if(this.data.containsKey("filename")){ + this.data["filename"] + } else { + this.data["title"] + } + return filename?.split(".")?.last() ?: "UNKNOWN" + } + return extension } } @@ -227,7 +244,8 @@ class Creator( @ColumnInfo(name = "parent") val parent: String, //itemKey of parent @ColumnInfo(name = "firstName") val firstName: String, @ColumnInfo(name = "lastName") val lastName: String, - @ColumnInfo(name = "creatorType") val creatorType: String + @ColumnInfo(name = "creatorType") val creatorType: String, + @ColumnInfo(name = "order") val order: Int ) : Parcelable { fun makeString(): String { return "${firstName} ${lastName}" diff --git a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Migration.kt b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Migration.kt index b9d665f8..04d2146c 100644 --- a/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Migration.kt +++ b/app/src/main/java/com/mickstarify/zooforzotero/ZoteroStorage/Database/Migration.kt @@ -36,4 +36,10 @@ val MIGRATION_3_4 = object : Migration(3,4) { database.execSQL("CREATE INDEX IF NOT EXISTS `index_ItemCollection_itemKey_collectionKey` ON `ItemCollection` (`itemKey`, `collectionKey`)") } +} + +val MIGRATION_4_5 = object: Migration(4,5){ + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `ItemCreator` ADD `order` INTEGER NOT NULL DEFAULT -1") + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_sync_setup.xml b/app/src/main/res/layout/activity_sync_setup.xml index ce564fd2..b6777853 100644 --- a/app/src/main/res/layout/activity_sync_setup.xml +++ b/app/src/main/res/layout/activity_sync_setup.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content" android:layout_marginLeft="24dp" android:layout_marginTop="16dp" - android:text="Pick your cloud storage provider" + android:text="@string/pick_your_cloud_storage_provider" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -28,49 +28,20 @@ android:layout_marginLeft="16dp" app:layout_constraintLeft_toLeftOf="parent"> - - - - - - - + android:text="@string/zotero_account_sync" /> @@ -82,7 +53,7 @@ android:layout_marginBottom="32dp" android:layout_marginRight="32dp" android:padding="12dp" - android:text="Proceed" + android:text="@string/proceed" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toRightOf="parent" /> @@ -91,7 +62,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="48dp" - android:text="I have only implemented support for the Zotero Account Syncing option. I will implement alternative syncing options in the future, sorry for the inconvenience." + android:text="@string/app_frontpage_disclaimer" app:layout_constraintEnd_toEndOf="@+id/btn_sync_proceed" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="@+id/radiogroup_cloudproviders" diff --git a/app/src/main/res/menu/activity_library_actionbar.xml b/app/src/main/res/menu/activity_library_actionbar.xml index b185ba44..f13ed86f 100644 --- a/app/src/main/res/menu/activity_library_actionbar.xml +++ b/app/src/main/res/menu/activity_library_actionbar.xml @@ -40,4 +40,10 @@ android:title="@string/zotero_save" app:showAsAction="collapseActionView" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bca060d7..42e9432d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,17 +20,9 @@ Default reply action - Sync email periodically - Download incoming attachments - Automatically download attachments for incoming emails - - Only download attachments when manually requested Settings Sync Method Syncing - Sync only new content - Downloading the entire library every time. Warning this is slow and wastes bandwidth. - Only syncing when changes are detected. This is recommended. Title Author This catalog is currently empty.\nPlease refresh your library. @@ -47,4 +39,10 @@ Zotero Save Loading your library Filter Menu + Force Resync + You must use the official Zotero Account syncing to use this app. + Proceed + Enter your Zotero API Key Manually + Zotero Account Sync + Pick your cloud storage provider diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index d11b0879..21219ad6 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -46,7 +46,7 @@ + android:title="Single press opens attachment" />