Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| def8630f8f |
@@ -1,9 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[syncthing-lite.stringsxml]
|
||||
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = app/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw, id:in
|
||||
@@ -13,14 +13,8 @@ example, mobile devices with limited storage available, wishing to access a sync
|
||||
|
||||
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
|
||||
|
||||
|
||||
[<img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80">](https://f-droid.org/packages/net.syncthing.lite/)
|
||||
[<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80">](https://play.google.com/store/apps/details?id=net.syncthing.lite)
|
||||
|
||||
## Translations
|
||||
|
||||
The project is translated on [Transifex](https://www.transifex.com/syncthing-android/syncthing-lite/).
|
||||
|
||||
## Building
|
||||
|
||||
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
|
||||
|
||||
+11
-32
@@ -1,7 +1,6 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
@@ -9,10 +8,10 @@ android {
|
||||
dataBinding.enabled = true
|
||||
defaultConfig {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 25
|
||||
versionCode 9
|
||||
versionName "0.2.1"
|
||||
versionCode 3
|
||||
versionName "0.1.2"
|
||||
multiDexEnabled true
|
||||
}
|
||||
sourceSets {
|
||||
@@ -22,46 +21,26 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile = {
|
||||
def path = System.getenv("SYNCTHING_LITE_RELEASE_STORE_FILE")
|
||||
return (path) ? file(path) : null
|
||||
}()
|
||||
storePassword System.getenv("SIGNING_PASSWORD") ?: ""
|
||||
keyAlias System.getenv("SYNCTHING_LITE_RELEASE_KEY_ALIAS") ?: ""
|
||||
keyPassword System.getenv("SIGNING_PASSWORD") ?: ""
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/*'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko-commons:$anko_version"
|
||||
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
|
||||
implementation "org.jetbrains.anko:anko-commons:0.10.4"
|
||||
kapt "com.android.databinding:compiler:$build_tools_version"
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
kapt "com.android.databinding:library:1.3.3"
|
||||
implementation ("com.github.Nutomic:syncthing-java:0.2.1") {
|
||||
implementation "com.android.support:cardview-v7:$support_version"
|
||||
implementation ("com.github.Nutomic:syncthing-java:0.1.2") {
|
||||
exclude group: 'commons-logging', module:'commons-logging'
|
||||
exclude group: 'commons-codec'
|
||||
exclude group: 'org.apache.httpcomponents', module:'httpclient'
|
||||
exclude group: 'org.slf4j'
|
||||
exclude group: 'ch.qos.logback'
|
||||
}
|
||||
// NOTE: httpclient-android seems to be used via reflection somehow. Removing this dependency
|
||||
// silently breaks the app.
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
|
||||
implementation 'sk.baka.slf4j:slf4j-handroid:1.7.26'
|
||||
implementation 'com.google.zxing:android-integration:3.3.0'
|
||||
implementation 'com.google.zxing:core:3.3.0'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.3'
|
||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||
implementation 'uk.co.markormesher:android-fab:2.0.0'
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
|
||||
<issue id="GoogleAppIndexingWarning" severity="ignore" />
|
||||
|
||||
<issue id="InvalidPackage" severity="ignore" />
|
||||
|
||||
<issue id="OldTargetApi" severity="ignore" />
|
||||
</lint>
|
||||
@@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.syncthing.lite">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -17,29 +18,10 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".activities.IntroActivity"
|
||||
android:theme="@style/Theme.Syncthing.NoActionBar"/>
|
||||
<activity android:name=".activities.FolderBrowserActivity"
|
||||
android:parentActivityName=".activities.MainActivity"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="net.syncthing.lite.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".library.SyncthingProvider"
|
||||
android:authorities="net.syncthing.lite.documents"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
<activity android:name=".activities.FilePickerActivity"
|
||||
android:parentActivityName=".activities.FolderBrowserActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import net.syncthing.lite.R
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Activity that allows selecting a directory in the local file system.
|
||||
*/
|
||||
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
|
||||
|
||||
private lateinit var mListView: ListView
|
||||
private lateinit var mFilesAdapter: FileAdapter
|
||||
private lateinit var mLocation: File
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_folder_picker)
|
||||
mListView = findViewById(android.R.id.list)
|
||||
mListView.onItemClickListener = this
|
||||
mListView.emptyView = findViewById(android.R.id.empty)
|
||||
mFilesAdapter = FileAdapter(this)
|
||||
mListView.adapter = mFilesAdapter
|
||||
|
||||
displayFolder(Environment.getExternalStorageDirectory())
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
|
||||
*/
|
||||
private fun displayFolder(location: File) {
|
||||
mLocation = location
|
||||
mFilesAdapter.clear()
|
||||
// In case we don't have read access to the location, just display nothing.
|
||||
val contents = location.listFiles() ?: arrayOf()
|
||||
|
||||
Arrays.sort(contents) { f1, f2 ->
|
||||
if (f1.isDirectory && f2.isFile)
|
||||
return@sort -1
|
||||
if (f1.isFile && f2.isDirectory)
|
||||
return@sort 1
|
||||
f1.name.compareTo(f2.name)
|
||||
}
|
||||
|
||||
for (f in contents) {
|
||||
mFilesAdapter.add(f)
|
||||
}
|
||||
mListView.adapter = mFilesAdapter
|
||||
}
|
||||
|
||||
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
|
||||
val f = mFilesAdapter.getItem(i)
|
||||
if (f!!.isDirectory) {
|
||||
displayFolder(f)
|
||||
} else {
|
||||
val intent = Intent()
|
||||
intent.data = Uri.fromFile(f)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val title = view.findViewById<TextView>(android.R.id.text1)
|
||||
val f = getItem(position)!!
|
||||
title.text = f.name
|
||||
val icon =
|
||||
if (f.isDirectory) R.drawable.ic_folder_black_24dp
|
||||
else R.drawable.ic_image_black_24dp
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mLocation != Environment.getExternalStorageDirectory()) {
|
||||
displayFolder(mLocation.parentFile)
|
||||
} else {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +1,45 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.google.common.base.Preconditions.checkArgument
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.utils.FileInfoOrdering
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.FolderContentsAdapter
|
||||
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
|
||||
import net.syncthing.lite.dialogs.FileDownloadDialog
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
import net.syncthing.lite.library.DownloadFileTask
|
||||
import net.syncthing.lite.library.UploadFileTask
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FolderBrowserActivity"
|
||||
private const val REQUEST_SELECT_UPLOAD_FILE = 171
|
||||
private val TAG = "FolderBrowserActivity"
|
||||
private val REQUEST_WRITE_STORAGE = 142
|
||||
private val REQUEST_SELECT_UPLOAD_FILE = 171
|
||||
|
||||
const val EXTRA_FOLDER_NAME = "folder_name"
|
||||
val EXTRA_FOLDER_NAME = "folder_name"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityFolderBrowserBinding
|
||||
private lateinit var indexBrowser: IndexBrowser
|
||||
private lateinit var adapter: FolderContentsAdapter
|
||||
private var runWhenPermissionsReceived: Runnable? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -41,10 +52,14 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
navigateToFolder(fileInfo)
|
||||
}
|
||||
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
|
||||
libraryHandler?.syncthingClient {
|
||||
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
|
||||
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
|
||||
}
|
||||
indexBrowser = syncthingClient().indexHandler
|
||||
.newIndexBrowserBuilder()
|
||||
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
|
||||
.includeParentInList(true)
|
||||
.allowParentInRoot(true)
|
||||
.setFolder(folder)
|
||||
.build()
|
||||
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -63,32 +78,30 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
|
||||
indexBrowser.folder, indexBrowser.currentPath,
|
||||
{ showFolderListView(indexBrowser.currentPath) }).show()
|
||||
}
|
||||
UploadFileTask(this, syncthingClient(), intent!!.data, indexBrowser.folder,
|
||||
indexBrowser.currentPath, { this.updateFolderListView() }).uploadFile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFolderListView(path: String) {
|
||||
indexBrowser.navigateToNearestPath(path)
|
||||
navigateToFolder(indexBrowser.currentPathInfo())
|
||||
navigateToFolder(indexBrowser.currentPathInfo)
|
||||
}
|
||||
|
||||
private fun navigateToFolder(fileInfo: FileInfo) {
|
||||
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
|
||||
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
|
||||
if (indexBrowser.isRoot && PathUtils.isParent(fileInfo.path)) {
|
||||
finish()
|
||||
} else {
|
||||
if (fileInfo.isDirectory()) {
|
||||
if (fileInfo.isDirectory) {
|
||||
indexBrowser.navigateTo(fileInfo)
|
||||
Log.d(TAG, "load folder cache bg")
|
||||
binding.listView.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
} else {
|
||||
Log.i(TAG, "pulling file = " + fileInfo)
|
||||
libraryHandler?.syncthingClient { FileDownloadDialog(this, it, fileInfo).show() }
|
||||
executeWithPermissions(
|
||||
Runnable { DownloadFileTask(this, syncthingClient(), fileInfo).downloadFile() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,17 +113,17 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
val list = indexBrowser.listFiles()
|
||||
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
|
||||
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
|
||||
assert(!list.isEmpty())//list must contain at least the 'parent' path
|
||||
checkArgument(!list.isEmpty())//list must contain at least the 'parent' path
|
||||
adapter.clear()
|
||||
adapter.addAll(list)
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.listView.setSelection(0)
|
||||
if (indexBrowser.isRoot())
|
||||
libraryHandler?.folderBrowser {
|
||||
supportActionBar?.title = it.getFolderInfo(indexBrowser.folder)?.label
|
||||
}
|
||||
else
|
||||
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
|
||||
val title =
|
||||
if (indexBrowser.isRoot)
|
||||
folderBrowser()?.getFolderInfo(indexBrowser.folder)?.label
|
||||
else
|
||||
indexBrowser.currentPathInfo.fileName
|
||||
supportActionBar!!.setTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +132,49 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun showUploadHereDialog() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "*/*"
|
||||
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
|
||||
executeWithPermissions(Runnable {
|
||||
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
super.onIndexUpdateComplete(folderInfo)
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
binding.indexUpdate.visibility = View.VISIBLE
|
||||
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder)
|
||||
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
|
||||
updateFolderListView()
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
binding.indexUpdate.visibility = View.GONE
|
||||
updateFolderListView()
|
||||
}
|
||||
|
||||
private fun executeWithPermissions(runnable: Runnable) {
|
||||
val permissionState = ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
if (permissionState != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
REQUEST_WRITE_STORAGE)
|
||||
runWhenPermissionsReceived = runnable
|
||||
} else {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
when (requestCode) {
|
||||
REQUEST_WRITE_STORAGE -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, R.string.toast_write_storage_permission_required,
|
||||
Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
runWhenPermissionsReceived!!.run()
|
||||
}
|
||||
runWhenPermissionsReceived = null
|
||||
}
|
||||
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.FragmentIntroOneBinding
|
||||
import net.syncthing.lite.databinding.FragmentIntroThreeBinding
|
||||
import net.syncthing.lite.databinding.FragmentIntroTwoBinding
|
||||
import net.syncthing.lite.fragments.SyncthingFragment
|
||||
import net.syncthing.lite.utils.FragmentIntentIntegrator
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import org.jetbrains.anko.intentFor
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Shown when a user first starts the app. Shows some info and helps the user to add their first
|
||||
* device and folder.
|
||||
*/
|
||||
class IntroActivity : AppIntro() {
|
||||
|
||||
/**
|
||||
* Initialize fragments and library parameters.
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Disable continue button on second slide until a valid device ID is entered.
|
||||
nextButton.setOnClickListener {
|
||||
val fragment = fragments[pager.currentItem]
|
||||
if (fragment !is IntroFragmentTwo || fragment.isDeviceIdValid()) {
|
||||
pager.goToNextSlide()
|
||||
}
|
||||
}
|
||||
|
||||
addSlide(IntroFragmentOne())
|
||||
addSlide(IntroFragmentTwo())
|
||||
addSlide(IntroFragmentThree())
|
||||
|
||||
setSeparatorColor(ContextCompat.getColor(this, android.R.color.primary_text_dark))
|
||||
showSkipButton(true)
|
||||
isProgressButtonEnabled = true
|
||||
pager.isPagingEnabled = false
|
||||
}
|
||||
|
||||
override fun onSkipPressed(currentFragment: Fragment) {
|
||||
onDonePressed(currentFragment)
|
||||
}
|
||||
|
||||
override fun onDonePressed(currentFragment: Fragment) {
|
||||
defaultSharedPreferences.edit().putBoolean(MainActivity.PREF_IS_FIRST_START, false).apply()
|
||||
startActivity(intentFor<MainActivity>())
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Display some simple welcome text.
|
||||
*/
|
||||
class IntroFragmentOne : SyncthingFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return DataBindingUtil.inflate<FragmentIntroOneBinding>(
|
||||
inflater, R.layout.fragment_intro_one, container, false).root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
context?.let { SyncthingActivity.checkLocalDiscoveryPort(it) }
|
||||
libraryHandler?.configuration { config ->
|
||||
config.localDeviceName = Util.getDeviceName()
|
||||
config.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display device ID entry field and QR scanner option.
|
||||
*/
|
||||
class IntroFragmentTwo : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentIntroTwoBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_two, container, false)
|
||||
binding.enterDeviceId!!.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@IntroFragmentTwo).initiateScan()
|
||||
}
|
||||
binding.enterDeviceId!!.scanQrCode.setImageResource(R.drawable.ic_qr_code_white_24dp)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
|
||||
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
|
||||
binding.enterDeviceId!!.deviceId.setText(scanResult.contents)
|
||||
binding.enterDeviceId!!.deviceIdHolder.isErrorEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entered device ID is valid. If yes, imports it and returns true. If not,
|
||||
* sets an error on the textview and returns false.
|
||||
*/
|
||||
fun isDeviceIdValid(): Boolean {
|
||||
return try {
|
||||
val deviceId = binding.enterDeviceId!!.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { })
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
binding.enterDeviceId!!.deviceId.error = getString(R.string.invalid_device_id)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until remote device connects with new folder.
|
||||
*/
|
||||
class IntroFragmentThree : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentIntroThreeBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
libraryHandler?.library { config, client, _ ->
|
||||
client.addOnConnectionChangedListener(this::onConnectionChanged)
|
||||
val deviceId = config.localDeviceId.deviceId
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
libraryHandler?.library { config, client, _ ->
|
||||
if (config.folders.isNotEmpty()) {
|
||||
client.removeOnConnectionChangedListener(this::onConnectionChanged)
|
||||
(activity as IntroActivity?)?.onDonePressed(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,38 +4,27 @@ import android.app.AlertDialog
|
||||
import android.content.res.Configuration
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.ActionBarDrawerToggle
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import android.view.View
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ActivityMainBinding
|
||||
import net.syncthing.lite.dialogs.DeviceIdDialog
|
||||
import net.syncthing.lite.fragments.DevicesFragment
|
||||
import net.syncthing.lite.fragments.FoldersFragment
|
||||
import net.syncthing.lite.fragments.SettingsFragment
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import org.jetbrains.anko.intentFor
|
||||
import net.syncthing.lite.fragments.SyncthingFragment
|
||||
import net.syncthing.lite.library.UpdateIndexTask
|
||||
|
||||
class MainActivity : SyncthingActivity() {
|
||||
|
||||
companion object {
|
||||
const val PREF_IS_FIRST_START = "net.syncthing.lite.activities.MainActivity.IS_FIRST_START"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var drawerToggle: ActionBarDrawerToggle? = null
|
||||
private var currentFragment: SyncthingFragment? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (defaultSharedPreferences.getBoolean(PREF_IS_FIRST_START, true)) {
|
||||
startActivity(intentFor<IntroActivity>())
|
||||
finish()
|
||||
}
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
||||
|
||||
drawerToggle = ActionBarDrawerToggle(
|
||||
@@ -61,6 +50,10 @@ class MainActivity : SyncthingActivity() {
|
||||
onNavigationItemSelectedListener(selection)
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
currentFragment?.onLibraryLoaded()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
drawerToggle!!.onConfigurationChanged(newConfig)
|
||||
@@ -79,10 +72,7 @@ class MainActivity : SyncthingActivity() {
|
||||
when (menuItem.itemId) {
|
||||
R.id.folders -> setContentFragment(FoldersFragment())
|
||||
R.id.devices -> setContentFragment(DevicesFragment())
|
||||
R.id.settings -> setContentFragment(SettingsFragment())
|
||||
R.id.device_id -> libraryHandler?.configuration { config ->
|
||||
DeviceIdDialog(this, config.localDeviceId).show()
|
||||
}
|
||||
R.id.update_index -> UpdateIndexTask(this, syncthingClient()).updateIndex()
|
||||
R.id.clear_index -> AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.clear_cache_and_index_title))
|
||||
.setMessage(getString(R.string.clear_cache_and_index_body))
|
||||
@@ -95,7 +85,8 @@ class MainActivity : SyncthingActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setContentFragment(fragment: Fragment) {
|
||||
private fun setContentFragment(fragment: SyncthingFragment) {
|
||||
currentFragment = fragment
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.content_frame, fragment)
|
||||
@@ -103,9 +94,17 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
}
|
||||
syncthingClient().clearCacheAndIndex()
|
||||
recreate()
|
||||
}
|
||||
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
binding.indexUpdate.visibility = View.VISIBLE
|
||||
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder) + " "
|
||||
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
binding.indexUpdate.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,87 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.LayoutInflater
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.configuration.ConfigurationService
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogLoadingBinding
|
||||
import net.syncthing.lite.library.InitLibraryTask
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.jetbrains.anko.contentView
|
||||
import org.slf4j.impl.HandroidLoggerAdapter
|
||||
|
||||
abstract class SyncthingActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
fun checkLocalDiscoveryPort(context: Context) {
|
||||
if (LibraryHandler.isListeningPortTaken) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.other_syncthing_instance_title)
|
||||
.setMessage(R.string.other_syncthing_instance_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
private var activityCount = 0
|
||||
private var libraryHandler: LibraryHandler? = null
|
||||
}
|
||||
|
||||
var libraryHandler: LibraryHandler? = null
|
||||
private set
|
||||
private var loadingDialog: AlertDialog? = null
|
||||
private var snackBar: Snackbar? = null
|
||||
|
||||
fun syncthingClient(): SyncthingClient {
|
||||
if (isDestroyed)
|
||||
throw IllegalStateException("activity is already destroyed")
|
||||
return libraryHandler!!.syncthingClient!!
|
||||
}
|
||||
|
||||
fun configuration(): ConfigurationService {
|
||||
if (isDestroyed)
|
||||
throw IllegalStateException("activity is already destroyed")
|
||||
return libraryHandler!!.configuration!!
|
||||
}
|
||||
|
||||
fun folderBrowser(): FolderBrowser? {
|
||||
if (isDestroyed)
|
||||
throw IllegalStateException("activity is already destroyed")
|
||||
return libraryHandler?.folderBrowser
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
|
||||
|
||||
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
|
||||
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
|
||||
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
|
||||
loadingDialog = AlertDialog.Builder(this)
|
||||
.setCancelable(false)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
LibraryHandler(this, this::onLibraryLoadedInternal,
|
||||
this::onIndexUpdateProgress, this::onIndexUpdateComplete)
|
||||
activityCount++
|
||||
if (libraryHandler == null) {
|
||||
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
|
||||
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
|
||||
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
|
||||
loadingDialog = AlertDialog.Builder(this)
|
||||
.setCancelable(false)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
InitLibraryTask(this, this::onLibraryLoaded, this::onIndexUpdateProgress, this::onIndexUpdateComplete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
libraryHandler?.close()
|
||||
loadingDialog?.dismiss()
|
||||
activityCount--
|
||||
Thread {
|
||||
if (activityCount == 0) {
|
||||
libraryHandler?.destroy()
|
||||
libraryHandler = null
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
if (!isDestroyed) {
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
private fun onLibraryLoaded(libraryHandler: LibraryHandler) {
|
||||
if (activityCount == 0)
|
||||
return
|
||||
|
||||
SyncthingActivity.libraryHandler = libraryHandler
|
||||
loadingDialog!!.cancel()
|
||||
onLibraryLoaded()
|
||||
}
|
||||
|
||||
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {
|
||||
val message = getString(R.string.index_update_progress_label, folderInfo.label, percentage)
|
||||
snackBar?.setText(message) ?: run {
|
||||
snackBar = Snackbar.make(contentView!!, message, Snackbar.LENGTH_INDEFINITE)
|
||||
snackBar?.show()
|
||||
}
|
||||
}
|
||||
open fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {}
|
||||
|
||||
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
snackBar?.dismiss()
|
||||
snackBar = null
|
||||
}
|
||||
open fun onIndexUpdateComplete() {}
|
||||
|
||||
open fun onLibraryLoaded() {
|
||||
checkLocalDiscoveryPort(this)
|
||||
}
|
||||
open fun onLibraryLoaded() {}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import com.google.common.collect.Lists
|
||||
import net.syncthing.java.core.beans.DeviceStats
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewDeviceBinding
|
||||
|
||||
class DevicesAdapter(context: Context) :
|
||||
ArrayAdapter<DeviceInfo>(context, R.layout.listview_device, mutableListOf()) {
|
||||
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, Lists.newArrayList()) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewDeviceBinding
|
||||
@@ -23,10 +24,10 @@ class DevicesAdapter(context: Context) :
|
||||
val deviceStats = getItem(position)
|
||||
binding.deviceName.text = deviceStats!!.name
|
||||
val icon =
|
||||
if (deviceStats.isConnected!!) {
|
||||
R.drawable.ic_laptop_green_24dp
|
||||
} else {
|
||||
R.drawable.ic_laptop_red_24dp
|
||||
when (deviceStats.status) {
|
||||
DeviceStats.DeviceStatus.OFFLINE -> R.drawable.ic_laptop_red_24dp
|
||||
DeviceStats.DeviceStatus.ONLINE_INACTIVE,
|
||||
DeviceStats.DeviceStatus.ONLINE_ACTIVE -> R.drawable.ic_laptop_green_24dp
|
||||
}
|
||||
binding.deviceIcon.setImageResource(icon)
|
||||
return binding.root
|
||||
|
||||
@@ -7,13 +7,14 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import com.google.common.collect.Lists
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewFileBinding
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
class FolderContentsAdapter(context: Context) :
|
||||
ArrayAdapter<FileInfo>(context, R.layout.listview_file, mutableListOf()) {
|
||||
ArrayAdapter<FileInfo>(context, R.layout.listview_file, Lists.newArrayList()) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewFileBinding =
|
||||
@@ -24,15 +25,15 @@ class FolderContentsAdapter(context: Context) :
|
||||
}
|
||||
val fileInfo = getItem(position)
|
||||
binding.fileLabel.text = fileInfo!!.fileName
|
||||
if (fileInfo.isDirectory()) {
|
||||
if (fileInfo.isDirectory) {
|
||||
binding.fileIcon.setImageResource(R.drawable.ic_folder_black_24dp)
|
||||
binding.fileSize.visibility = View.GONE
|
||||
} else {
|
||||
binding.fileIcon.setImageResource(R.drawable.ic_image_black_24dp)
|
||||
binding.fileSize.visibility = View.VISIBLE
|
||||
binding.fileSize.text = context.getString(R.string.file_info,
|
||||
FileUtils.byteCountToDisplaySize(fileInfo.size!!),
|
||||
DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
binding.fileSize.text = (FileUtils.byteCountToDisplaySize(fileInfo.size!!)
|
||||
+ context.getString(R.string.last_modified)
|
||||
+ DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewFolderBinding
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
|
||||
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
|
||||
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
|
||||
@@ -22,13 +23,15 @@ class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderSt
|
||||
} else {
|
||||
DataBindingUtil.bind(v)
|
||||
}
|
||||
val folderInfo = getItem(position)!!.first
|
||||
val folderStats = getItem(position)!!.second
|
||||
binding.folderName.text = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
|
||||
|
||||
binding.folderLastmodInfo.text = context.getString(R.string.last_modified_time,
|
||||
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
binding.folderContentInfo.text = context.getString(R.string.folder_content_info, folderStats.describeSize(), folderStats.fileCount, folderStats.dirCount)
|
||||
val folderInfo = getItem(position)!!.left
|
||||
val folderStats = getItem(position)!!.right
|
||||
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
|
||||
binding.folderLastmodInfo.text =
|
||||
if (folderStats.lastUpdate == null)
|
||||
context.getString(R.string.last_modified_unknown)
|
||||
else context.getString(R.string.last_modified_known) + " " +
|
||||
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
|
||||
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogDeviceIdBinding
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
|
||||
class DeviceIdDialog(private val context: Context, private val deviceId: DeviceId) {
|
||||
|
||||
private val Tag = "DeviceIdDialog"
|
||||
|
||||
private val binding = DataBindingUtil.inflate<DialogDeviceIdBinding>(
|
||||
LayoutInflater.from(context), R.layout.dialog_device_id, null, false)
|
||||
|
||||
fun show() {
|
||||
generateQrCode()
|
||||
binding.deviceId.text = deviceId.deviceId
|
||||
// Make QR code/progress bar views rectangular based on match_parent height.
|
||||
binding.progressBar.post {
|
||||
binding.progressBar.minimumHeight = binding.progressBar.width
|
||||
binding.qrCode.minimumHeight = binding.progressBar.width
|
||||
}
|
||||
binding.deviceId.setOnClickListener({ copyDeviceId() })
|
||||
binding.share.setOnClickListener { }
|
||||
binding.share.setOnClickListener({ shareDeviceId() })
|
||||
|
||||
val qrCodeDialog = AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.device_id))
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
|
||||
qrCodeDialog.show()
|
||||
}
|
||||
|
||||
private fun generateQrCode() {
|
||||
doAsync {
|
||||
val writer = QRCodeWriter()
|
||||
try {
|
||||
val bitMatrix = writer.encode(deviceId.deviceId, BarcodeFormat.QR_CODE, 512, 512)
|
||||
val width = bitMatrix.width
|
||||
val height = bitMatrix.height
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
|
||||
}
|
||||
}
|
||||
async(UI) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.qrCode.visibility = View.VISIBLE
|
||||
binding.qrCode.setImageBitmap(bmp)
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
Log.w(Tag, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyDeviceId() {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(context.getString(R.string.device_id), deviceId.deviceId)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(context, context.getString(R.string.device_id_copied), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareDeviceId() {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, deviceId.deviceId)
|
||||
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share_device_id_chooser)))
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.content.FileProvider
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.DownloadFileTask
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.newTask
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.File
|
||||
|
||||
class FileDownloadDialog(private val context: Context, private val syncthingClient: SyncthingClient,
|
||||
private val fileInfo: FileInfo) {
|
||||
|
||||
private val Tag = "FileDownloadDialog"
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var downloadFileTask: DownloadFileTask? = null
|
||||
|
||||
fun show() {
|
||||
showDialog()
|
||||
doAsync {
|
||||
downloadFileTask = DownloadFileTask(context, syncthingClient, fileInfo,
|
||||
this@FileDownloadDialog::onProgress, this@FileDownloadDialog::onComplete,
|
||||
this@FileDownloadDialog::onError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
progressDialog = ProgressDialog(context)
|
||||
progressDialog.setMessage(context.getString(R.string.dialog_downloading_file, fileInfo.fileName))
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
progressDialog.setCancelable(true)
|
||||
progressDialog.setOnCancelListener { downloadFileTask?.cancel() }
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(downloadFileTask: DownloadFileTask, fileDownloadObserver: BlockPuller.FileDownloadObserver) {
|
||||
async(UI) {
|
||||
progressDialog.isIndeterminate = false
|
||||
progressDialog.max = (fileInfo.size as Long).toInt()
|
||||
progressDialog.progress = (fileDownloadObserver.progress() * fileInfo.size!!).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete(file: File) {
|
||||
async(UI) {
|
||||
progressDialog.dismiss()
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val uri = FileProvider.getUriForFile(this@FileDownloadDialog.context, "net.syncthing.lite.fileprovider", file)
|
||||
intent.setDataAndType(uri, mimeType)
|
||||
intent.newTask()
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
try {
|
||||
this@FileDownloadDialog.context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
this@FileDownloadDialog.context.toast(R.string.toast_open_file_failed)
|
||||
Log.w(Tag, "No handler found for file " + file.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError() {
|
||||
async(UI) {
|
||||
progressDialog.cancel()
|
||||
this@FileDownloadDialog.context.toast(R.string.toast_file_download_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.UploadFileTask
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.toast
|
||||
|
||||
class FileUploadDialog(private val context: Context, private val syncthingClient: SyncthingClient,
|
||||
private val localFile: Uri, private val syncthingFolder: String,
|
||||
private val syncthingSubFolder: String,
|
||||
private val onUploadCompleteListener: () -> Unit) {
|
||||
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var uploadFileTask: UploadFileTask? = null
|
||||
|
||||
fun show() {
|
||||
showDialog()
|
||||
doAsync {
|
||||
uploadFileTask = UploadFileTask(context, syncthingClient, localFile, syncthingFolder,
|
||||
syncthingSubFolder, this@FileUploadDialog::onProgress,
|
||||
this@FileUploadDialog::onComplete, this@FileUploadDialog::onError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
progressDialog = ProgressDialog(context)
|
||||
progressDialog.setMessage(context.getString(R.string.dialog_uploading_file, Util.getContentFileName(context, localFile)))
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
progressDialog.setCancelable(true)
|
||||
progressDialog.setOnCancelListener { uploadFileTask?.cancel() }
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
|
||||
async(UI) {
|
||||
progressDialog.isIndeterminate = false
|
||||
progressDialog.progress = observer.progressPercentage()
|
||||
progressDialog.max = 100
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete() {
|
||||
async(UI) {
|
||||
progressDialog.dismiss()
|
||||
this@FileUploadDialog.context.toast(R.string.toast_upload_complete)
|
||||
onUploadCompleteListener()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError() {
|
||||
async(UI) {
|
||||
progressDialog.dismiss()
|
||||
this@FileUploadDialog.context.toast(R.string.toast_file_upload_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,47 +5,45 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.DeviceStats
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.DevicesAdapter
|
||||
import net.syncthing.lite.databinding.FragmentDevicesBinding
|
||||
import net.syncthing.lite.databinding.ViewEnterDeviceIdBinding
|
||||
import net.syncthing.lite.library.UpdateIndexTask
|
||||
import net.syncthing.lite.utils.FragmentIntentIntegrator
|
||||
import net.syncthing.lite.utils.Util
|
||||
import java.io.IOException
|
||||
import org.apache.commons.lang3.StringUtils.isBlank
|
||||
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
|
||||
import uk.co.markormesher.android_fab.SpeedDialMenuItem
|
||||
import java.security.InvalidParameterException
|
||||
|
||||
class DevicesFragment : SyncthingFragment() {
|
||||
|
||||
companion object {
|
||||
private val TAG = "DevicesFragment"
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentDevicesBinding
|
||||
private lateinit var adapter: DevicesAdapter
|
||||
private var addDeviceDialog: AlertDialog? = null
|
||||
private var addDeviceDialogBinding: ViewEnterDeviceIdBinding? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_devices, container, false)
|
||||
binding.list.emptyView = binding.empty
|
||||
binding.addDevice.setOnClickListener { showDialog() }
|
||||
binding.fab.speedDialMenuAdapter = FabMenuAdapter()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener { updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener{ updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
override fun onLibraryLoadedAndActivityCreated() {
|
||||
initDeviceList()
|
||||
updateDeviceList()
|
||||
}
|
||||
@@ -54,70 +52,87 @@ class DevicesFragment : SyncthingFragment() {
|
||||
adapter = DevicesAdapter(context!!)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemLongClickListener { _, _, position, _ ->
|
||||
val device = adapter.getItem(position)
|
||||
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(getString(R.string.remove_device_title, device.name))
|
||||
.setMessage(getString(R.string.remove_device_message, device.deviceId.deviceId.substring(0, 7)))
|
||||
.setTitle(getString(R.string.remove_device_title) + " " + deviceId.substring(0, 7) + "?")
|
||||
.setMessage(getString(R.string.remove_device_body_1) + " " + deviceId.substring(0, 7) + " " + getString(R.string.remove_device_body_2))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
libraryHandler?.configuration { config ->
|
||||
config.peers = config.peers.filterNot { it.deviceId == device.deviceId }.toSet()
|
||||
config.persistLater()
|
||||
updateDeviceList()
|
||||
}
|
||||
}
|
||||
getSyncthingActivity().configuration().edit().removePeer(deviceId).persistLater() }
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDeviceList() {
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
adapter.clear()
|
||||
adapter.addAll(syncthingClient.getPeerStatus())
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
adapter.clear()
|
||||
adapter.addAll(getSyncthingActivity().syncthingClient().devicesHandler.deviceStatsList)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
// Check if this was a QR code scan.
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
|
||||
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
|
||||
addDeviceDialogBinding?.deviceId?.setText(scanResult.contents)
|
||||
if (scanResult != null) {
|
||||
val deviceId = scanResult.contents
|
||||
if (!isBlank(deviceId)) {
|
||||
importDeviceId(deviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
addDeviceDialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.view_enter_device_id, null, false)
|
||||
addDeviceDialogBinding?.let { binding ->
|
||||
binding.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
}
|
||||
binding.deviceId.post {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
private fun importDeviceId(deviceId: String) {
|
||||
try {
|
||||
KeystoreHandler.validateDeviceId(deviceId)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Toast.makeText(context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
addDeviceDialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
val modified = getSyncthingActivity().configuration().edit().addPeers(DeviceInfo(deviceId, null))
|
||||
if (modified) {
|
||||
getSyncthingActivity().configuration().edit().persistLater()
|
||||
Toast.makeText(context, getString(R.string.device_import_success) + " " + deviceId, Toast.LENGTH_SHORT).show()
|
||||
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
|
||||
UpdateIndexTask(context!!, getSyncthingActivity().syncthingClient()).updateIndex()
|
||||
} else {
|
||||
Toast.makeText(context, getString(R.string.device_already_known) + " " + deviceId, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
addDeviceDialog?.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
?.setOnClickListener {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { updateDeviceList() })
|
||||
addDeviceDialog?.dismiss()
|
||||
} catch (e: IOException) {
|
||||
binding.deviceId.error = getString(R.string.invalid_device_id)
|
||||
}
|
||||
private inner class FabMenuAdapter : SpeedDialMenuAdapter() {
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getMenuItem(context: Context, position: Int): SpeedDialMenuItem {
|
||||
when (position) {
|
||||
0 -> return SpeedDialMenuItem(context, R.drawable.ic_qr_code_white_24dp, R.string.scan_qr_code)
|
||||
1 -> return SpeedDialMenuItem(context, R.drawable.ic_edit_white_24dp, R.string.enter_device_id)
|
||||
}
|
||||
throw InvalidParameterException()
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(position: Int): Boolean {
|
||||
when (position) {
|
||||
0 -> FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
1 -> {
|
||||
val editText = EditText(context)
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> importDeviceId(editText.text.toString()) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,23 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.common.collect.Lists
|
||||
import com.google.common.collect.Ordering
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
import net.syncthing.lite.adapters.FoldersListAdapter
|
||||
import net.syncthing.lite.databinding.FragmentFoldersBinding
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.jetbrains.anko.intentFor
|
||||
import java.util.*
|
||||
|
||||
class FoldersFragment : SyncthingFragment() {
|
||||
|
||||
private val TAG = "FoldersFragment"
|
||||
companion object {
|
||||
private val TAG = "FoldersFragment"
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentFoldersBinding
|
||||
|
||||
@@ -26,26 +33,21 @@ class FoldersFragment : SyncthingFragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
override fun onLibraryLoadedAndActivityCreated() {
|
||||
showAllFoldersListView()
|
||||
}
|
||||
|
||||
private fun showAllFoldersListView() {
|
||||
libraryHandler?.folderBrowser { folderBrowser ->
|
||||
val list = folderBrowser.folderInfoAndStatsList()
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
|
||||
val adapter = FoldersListAdapter(context, list)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemClickListener { _, _, position, _ ->
|
||||
val folder = adapter.getItem(position)!!.first.folderId
|
||||
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
|
||||
startActivity(intent)
|
||||
}
|
||||
val list = Lists.newArrayList(getSyncthingActivity().folderBrowser()!!.folderInfoAndStatsList)
|
||||
Collections.sort(list, Ordering.natural<Comparable<String>>()
|
||||
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
|
||||
val adapter = FoldersListAdapter(context, list)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemClickListener { _, _, position, _ ->
|
||||
val folder = adapter.getItem(position)!!.left.folder
|
||||
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
super.onIndexUpdateComplete(folderInfo)
|
||||
showAllFoldersListView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
|
||||
val localDeviceName = findPreference("local_device_name") as EditTextPreference
|
||||
val appVersion = findPreference("app_version")
|
||||
|
||||
(activity as SyncthingActivity?)?.let { activity ->
|
||||
val versionName = activity.packageManager.getPackageInfo(activity.packageName, 0)?.versionName
|
||||
appVersion.summary = versionName
|
||||
|
||||
activity.libraryHandler?.configuration { localDeviceName.text = it.localDeviceName }
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
activity.libraryHandler?.configuration { conf ->
|
||||
conf.localDeviceName = localDeviceName.text
|
||||
conf.persistLater()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,33 +2,30 @@ package net.syncthing.lite.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
|
||||
/**
|
||||
* Handle connection to [[SyncthingActivity]], and make sure device rotation are handled correctly.
|
||||
*/
|
||||
abstract class SyncthingFragment : Fragment() {
|
||||
|
||||
var libraryHandler: LibraryHandler? = null
|
||||
private set
|
||||
protected fun getSyncthingActivity() = activity as SyncthingActivity
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
|
||||
this::onIndexUpdateComplete)
|
||||
checkConditions()
|
||||
}
|
||||
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
onLibraryLoaded()
|
||||
fun onLibraryLoaded() {
|
||||
checkConditions()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
libraryHandler?.close()
|
||||
private fun checkConditions() {
|
||||
if (activity != null && getSyncthingActivity().folderBrowser() != null ) {
|
||||
onLibraryLoadedAndActivityCreated()
|
||||
}
|
||||
}
|
||||
|
||||
open fun onLibraryLoaded() {}
|
||||
|
||||
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
|
||||
|
||||
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
|
||||
open fun onLibraryLoadedAndActivityCreated() {
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,107 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.support.annotation.StringRes
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.newTask
|
||||
import org.jetbrains.anko.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class DownloadFileTask(private val context: Context, syncthingClient: SyncthingClient,
|
||||
private val fileInfo: FileInfo,
|
||||
private val onProgress: (DownloadFileTask, BlockPuller.FileDownloadObserver) -> Unit,
|
||||
private val onComplete: (File) -> Unit,
|
||||
private val onError: () -> Unit) {
|
||||
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient,
|
||||
private val mFileInfo: FileInfo) {
|
||||
|
||||
private val Tag = "DownloadFileTask"
|
||||
private var isCancelled = false
|
||||
private val TAG = "DownloadFileTask"
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var cancelled = false
|
||||
|
||||
init {
|
||||
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
|
||||
val observer = blockPuller.pullFile(fileInfo)
|
||||
onProgress(this, observer)
|
||||
fun downloadFile() {
|
||||
showDialog()
|
||||
mSyncthingClient.pullFile(mFileInfo, { observer ->
|
||||
onProgress(observer)
|
||||
try {
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPuller
|
||||
while (!observer.isCompleted) {
|
||||
if (cancelled)
|
||||
return@pullFile
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i("pullFile", "download progress = " + observer.progressMessage())
|
||||
onProgress(this, observer)
|
||||
Log.i("pullFile", "download progress = " + observer.progressMessage)
|
||||
onProgress(observer)
|
||||
}
|
||||
|
||||
val outputFile = File("${context.externalCacheDir}/${fileInfo.folder}/${fileInfo.path}")
|
||||
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
|
||||
Log.i(Tag, "Downloaded file $fileInfo")
|
||||
val outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val outputFile = File(outputDir, mFileInfo.fileName)
|
||||
FileUtils.copyInputStreamToFile(observer.inputStream, outputFile)
|
||||
Log.i(TAG, "downloaded file = " + mFileInfo.path)
|
||||
onComplete(outputFile)
|
||||
} catch (e: IOException) {
|
||||
onError()
|
||||
Log.w(Tag, "Failed to download file $fileInfo", e)
|
||||
onError(R.string.toast_file_download_failed)
|
||||
Log.w(TAG, "Failed to download file " + mFileInfo, e)
|
||||
} catch (e: InterruptedException) {
|
||||
onError(R.string.toast_file_download_failed)
|
||||
Log.w(TAG, "Failed to download file " + mFileInfo, e)
|
||||
}
|
||||
}, { onError() })
|
||||
}) { onError(R.string.toast_file_download_failed) }
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
isCancelled = true
|
||||
private fun showDialog() {
|
||||
progressDialog = ProgressDialog(mContext)
|
||||
progressDialog.setMessage(mContext.getString(R.string.dialog_downloading_file, mFileInfo.fileName))
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
progressDialog.setCancelable(true)
|
||||
progressDialog.setOnCancelListener { cancelled = true }
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
|
||||
doAsync {
|
||||
uiThread {
|
||||
progressDialog.isIndeterminate = false
|
||||
progressDialog.max = (mFileInfo.size as Long).toInt()
|
||||
progressDialog.progress = (fileDownloadObserver.progress * mFileInfo.size!!).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete(file: File) {
|
||||
progressDialog.dismiss()
|
||||
if (cancelled)
|
||||
return
|
||||
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.fromFile(file), mimeType)
|
||||
intent.newTask()
|
||||
try {
|
||||
mContext.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
onError(R.string.toast_open_file_failed)
|
||||
Log.w(TAG, "No handler found for file " + file.name, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun onError(@StringRes error: Int) {
|
||||
doAsync {
|
||||
uiThread {
|
||||
progressDialog.dismiss()
|
||||
mContext.toast(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
class InitLibraryTask(private val context: Context, private val onLibraryLoaded: (LibraryHandler) -> Unit,
|
||||
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
|
||||
private val onIndexUpdateCompleteListener: () -> Unit) {
|
||||
|
||||
private val TAG = "InitLibraryTask"
|
||||
|
||||
init {
|
||||
doAsync {
|
||||
val libraryHandler = LibraryHandler()
|
||||
libraryHandler.init(context)
|
||||
libraryHandler.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
onIndexUpdateProgressListener(folder, percentage)
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
onIndexUpdateCompleteListener()
|
||||
}
|
||||
})
|
||||
//trigger update if last was more than 10mins ago
|
||||
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
|
||||
val lastUpdateTimeAgo = Date().time - lastUpdateMillis
|
||||
if (lastUpdateMillis == -1L || lastUpdateTimeAgo > 10 * 60 * 1000) {
|
||||
Log.d(TAG, "trigger index update, last was " + Date(lastUpdateMillis))
|
||||
UpdateIndexTask(context, libraryHandler.syncthingClient!!).updateIndex()
|
||||
}
|
||||
uiThread {
|
||||
onLibraryLoaded(libraryHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +1,82 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import com.google.common.eventbus.Subscribe
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.bep.IndexHandler
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.IndexInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.SocketException
|
||||
import java.util.*
|
||||
import net.syncthing.java.core.configuration.ConfigurationService
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
|
||||
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
|
||||
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit) {
|
||||
class LibraryHandler {
|
||||
|
||||
companion object {
|
||||
private var instanceCount = 0
|
||||
private var configuration: Configuration? = null
|
||||
private var syncthingClient: SyncthingClient? = null
|
||||
private var folderBrowser: FolderBrowser? = null
|
||||
private val callbacks = ArrayList<(Configuration, SyncthingClient, FolderBrowser) -> Unit>()
|
||||
private var isLoading = false
|
||||
var isListeningPortTaken = false
|
||||
private val TAG = "LibConnectionHandler"
|
||||
|
||||
private var mOnIndexUpdatedListener: OnIndexUpdatedListener? = null
|
||||
var configuration: ConfigurationService? = null
|
||||
private set
|
||||
var syncthingClient: SyncthingClient? = null
|
||||
private set
|
||||
var folderBrowser: FolderBrowser? = null
|
||||
private set
|
||||
|
||||
interface OnIndexUpdatedListener {
|
||||
fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int)
|
||||
fun onIndexUpdateComplete()
|
||||
}
|
||||
|
||||
private val TAG = "LibraryHandler"
|
||||
|
||||
init {
|
||||
instanceCount++
|
||||
if (configuration == null && !isLoading) {
|
||||
isLoading = true
|
||||
doAsync {
|
||||
checkIsListeningPortTaken()
|
||||
init(context)
|
||||
async(UI) {
|
||||
onLibraryLoaded(this@LibraryHandler)
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
onLibraryLoaded(this)
|
||||
}
|
||||
|
||||
syncthingClient {
|
||||
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
|
||||
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
|
||||
async(UI) {
|
||||
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
|
||||
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
|
||||
|
||||
async(UI) {
|
||||
onIndexUpdateCompleteListener(folderInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun init(context: Context) {
|
||||
val configuration = Configuration(configFolder = context.filesDir)
|
||||
val syncthingClient = SyncthingClient(configuration)
|
||||
val folderBrowser = syncthingClient.indexHandler.newFolderBrowser()
|
||||
|
||||
if (instanceCount == 0) {
|
||||
Log.d(TAG, "All LibraryHandler instances were closed during init")
|
||||
syncthingClient.close()
|
||||
folderBrowser.close()
|
||||
}
|
||||
|
||||
async(UI) {
|
||||
callbacks.forEach { it(configuration, syncthingClient, folderBrowser) }
|
||||
}
|
||||
LibraryHandler.configuration = configuration
|
||||
LibraryHandler.syncthingClient = syncthingClient
|
||||
LibraryHandler.folderBrowser = folderBrowser
|
||||
}
|
||||
|
||||
fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
|
||||
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
|
||||
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
|
||||
|
||||
// https://stackoverflow.com/a/35522422/1837158
|
||||
fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? {
|
||||
return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
|
||||
}
|
||||
safeLet(configuration, syncthingClient, folderBrowser) { c, s, f ->
|
||||
callback(c, s, f)
|
||||
} ?: run {
|
||||
if (isLoading) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncthingClient(callback: (SyncthingClient) -> Unit) {
|
||||
library { _, s, _ -> callback(s) }
|
||||
}
|
||||
|
||||
fun configuration(callback: (Configuration) -> Unit) {
|
||||
library { c, _, _ -> callback(c) }
|
||||
}
|
||||
|
||||
fun folderBrowser(callback: (FolderBrowser) -> Unit) {
|
||||
library { _, _, f -> callback(f) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if listening port for local discovery is taken by another app. Do this check here to
|
||||
* avoid adding another callback.
|
||||
*/
|
||||
private fun checkIsListeningPortTaken() {
|
||||
fun init(context: Context) {
|
||||
configuration = ConfigurationService.newLoader()
|
||||
.setCache(File(context.externalCacheDir, "cache"))
|
||||
.setDatabase(File(context.getExternalFilesDir(null), "database"))
|
||||
.loadFrom(File(context.getExternalFilesDir(null), "config.properties"))
|
||||
configuration!!.edit().setDeviceName(Util.getDeviceName())
|
||||
try {
|
||||
DatagramSocket(21027, InetAddress.getByName("0.0.0.0")).close()
|
||||
} catch (e: SocketException) {
|
||||
Log.w(TAG, e)
|
||||
isListeningPortTaken = true
|
||||
FileUtils.cleanDirectory(configuration!!.temp)
|
||||
} catch (ex: IOException) {
|
||||
Log.e(TAG, "error", ex)
|
||||
destroy()
|
||||
}
|
||||
|
||||
KeystoreHandler.newLoader().loadAndStore(configuration!!)
|
||||
configuration!!.edit().persistLater()
|
||||
Log.i(TAG, "loaded mConfiguration = " + configuration!!.newWriter().dumpToString())
|
||||
Log.i(TAG, "storage space = " + configuration!!.storageInfo.dumpAvailableSpace())
|
||||
syncthingClient = net.syncthing.java.client.SyncthingClient(configuration!!)
|
||||
//TODO listen for device events, update device list
|
||||
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters index update listener and decreases instance count.
|
||||
*
|
||||
* We wait a bit before closing [[syncthingClient]] etc, in case LibraryHandler is opened again
|
||||
* soon (eg in case of device rotation).
|
||||
*/
|
||||
fun close() {
|
||||
syncthingClient {
|
||||
try {
|
||||
it.indexHandler.unregisterOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
|
||||
it.indexHandler.unregisterOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignored, no idea why this is thrown
|
||||
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
|
||||
mOnIndexUpdatedListener = onIndexUpdatedListener
|
||||
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
|
||||
|
||||
@Subscribe
|
||||
fun handleIndexRecordAquiredEvent(event: IndexHandler.IndexRecordAquiredEvent) {
|
||||
val folder = syncthingClient!!.indexHandler.getFolderInfo(event.folder)
|
||||
val indexInfo = event.indexInfo
|
||||
event.newRecords.size
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
mOnIndexUpdatedListener!!.onIndexUpdateProgress(folder, (indexInfo.completed * 100).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
instanceCount--
|
||||
Handler().postDelayed({
|
||||
Thread {
|
||||
if (instanceCount == 0) {
|
||||
folderBrowser?.close()
|
||||
folderBrowser = null
|
||||
syncthingClient?.close()
|
||||
syncthingClient = null
|
||||
configuration = null
|
||||
}
|
||||
}.start()
|
||||
}, 60 * 1000)
|
||||
@Subscribe
|
||||
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
|
||||
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
|
||||
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
folderBrowser!!.close()
|
||||
syncthingClient!!.close()
|
||||
configuration!!.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import android.util.Log
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.R
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URLConnection
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class SyncthingProvider : DocumentsProvider() {
|
||||
|
||||
companion object {
|
||||
private const val Tag = "SyncthingProvider"
|
||||
private val DefaultRootProjection = arrayOf(
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID,
|
||||
Root.COLUMN_ICON)
|
||||
private val DefaultDocumentProjection = arrayOf(
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_SIZE,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_FLAGS)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
Log.d(Tag, "onCreate()")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getLibraryHandler(): LibraryHandler {
|
||||
val latch = CountDownLatch(1)
|
||||
val libraryHandler = LibraryHandler(context, { latch.countDown() }, { _, _ -> }, {})
|
||||
latch.await()
|
||||
return libraryHandler
|
||||
}
|
||||
|
||||
override fun queryRoots(projection: Array<String>?): Cursor {
|
||||
Log.d(Tag, "queryRoots($projection)")
|
||||
val latch = CountDownLatch(1)
|
||||
var folders: List<Pair<FolderInfo, FolderStats>>? = null
|
||||
getLibraryHandler().folderBrowser { folderBrowser ->
|
||||
folders = folderBrowser.folderInfoAndStatsList()
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
|
||||
val result = MatrixCursor(projection ?: DefaultRootProjection)
|
||||
folders!!.forEach { folder ->
|
||||
val row = result.newRow()
|
||||
row.add(Root.COLUMN_ROOT_ID, folder.first.folderId)
|
||||
row.add(Root.COLUMN_SUMMARY, folder.first.label)
|
||||
row.add(Root.COLUMN_FLAGS, 0)
|
||||
row.add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.first))
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
|
||||
sortOrder: String?): Cursor {
|
||||
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
|
||||
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
|
||||
getIndexBrowser(getFolderIdForDocId(parentDocumentId))
|
||||
.listFiles(getPathForDocId(parentDocumentId))
|
||||
.forEach { fileInfo ->
|
||||
includeFile(result, fileInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
|
||||
Log.d(Tag, "queryDocument($documentId, $projection)")
|
||||
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
|
||||
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
|
||||
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
|
||||
includeFile(result, fileInfo)
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?):
|
||||
ParcelFileDescriptor {
|
||||
Log.d(Tag, "openDocument($documentId, $mode, $signal)")
|
||||
val fileInfo = FileInfo(folder = getFolderIdForDocId(documentId),
|
||||
path = getPathForDocId(documentId), type = FileInfo.FileType.FILE)
|
||||
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
var outputFile: File? = null
|
||||
getLibraryHandler().syncthingClient { syncthingClient ->
|
||||
DownloadFileTask(context, syncthingClient, fileInfo,
|
||||
{ t, _ -> if (signal?.isCanceled == true) t.cancel() }, {
|
||||
outputFile = it
|
||||
latch.countDown()
|
||||
}, {})
|
||||
}
|
||||
latch.await()
|
||||
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
}
|
||||
|
||||
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
|
||||
val row = result.newRow()
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
|
||||
row.add(Document.COLUMN_SIZE, fileInfo.size)
|
||||
val mime = if (fileInfo.isDirectory()) Document.MIME_TYPE_DIR
|
||||
else URLConnection.guessContentTypeFromName(fileInfo.fileName)
|
||||
row.add(Document.COLUMN_MIME_TYPE, mime)
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
|
||||
row.add(Document.COLUMN_FLAGS, 0)
|
||||
}
|
||||
|
||||
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
|
||||
|
||||
private fun getPathForDocId(docId: String) = docId.split(":")[1]
|
||||
|
||||
private fun getDocIdForFile(folderInfo: FolderInfo) = folderInfo.folderId + ":"
|
||||
|
||||
private fun getDocIdForFile(fileInfo: FileInfo) = fileInfo.folder + ":" + fileInfo.path
|
||||
|
||||
private fun getIndexBrowser(folderId: String): IndexBrowser {
|
||||
val latch = CountDownLatch(1)
|
||||
var indexBrowser: IndexBrowser? = null
|
||||
getLibraryHandler().syncthingClient {
|
||||
indexBrowser = it.indexHandler.newIndexBrowser(folderId)
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
return indexBrowser!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.lite.R
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
class UpdateIndexTask(private val androidContext: Context, private val syncthingClient: SyncthingClient) {
|
||||
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(androidContext)
|
||||
|
||||
fun updateIndex() {
|
||||
if (sIndexUpdateInProgress)
|
||||
return
|
||||
|
||||
sIndexUpdateInProgress = true
|
||||
syncthingClient.updateIndexFromPeers { _, failures ->
|
||||
sIndexUpdateInProgress = false
|
||||
if (failures.isEmpty()) {
|
||||
showToast(androidContext.getString(R.string.toast_index_update_successful))
|
||||
} else {
|
||||
showToast(androidContext.getString(R.string.toast_index_update_failed, failures.size))
|
||||
}
|
||||
mPreferences.edit()
|
||||
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
doAsync {
|
||||
uiThread {
|
||||
androidContext.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val LAST_INDEX_UPDATE_TS_PREF = "LAST_INDEX_UPDATE_TS"
|
||||
|
||||
private var sIndexUpdateInProgress: Boolean = false
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,103 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.io.IOException
|
||||
|
||||
// TODO: this should be an IntentService with notification
|
||||
class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
|
||||
localFile: Uri, private val syncthingFolder: String,
|
||||
class UploadFileTask(private val context: Context, private val syncthingClient: SyncthingClient,
|
||||
private val localFile: Uri, private val syncthingFolder: String,
|
||||
syncthingSubFolder: String,
|
||||
private val onProgress: (BlockPusher.FileUploadObserver) -> Unit,
|
||||
private val onComplete: () -> Unit,
|
||||
private val onError: () -> Unit) {
|
||||
private val onUploadCompleteListener: () -> Unit) {
|
||||
|
||||
private val TAG = "UploadFileTask"
|
||||
companion object {
|
||||
private val TAG = "UploadFileTask"
|
||||
}
|
||||
|
||||
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
|
||||
private val uploadStream = context.contentResolver.openInputStream(localFile)
|
||||
private val fileName = Util.getContentFileName(context, localFile)
|
||||
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
|
||||
|
||||
private var isCancelled = false
|
||||
private lateinit var mProgressDialog: ProgressDialog
|
||||
private var mCancelled = false
|
||||
|
||||
init {
|
||||
fun uploadFile() {
|
||||
createDialog()
|
||||
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
|
||||
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
|
||||
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
|
||||
onProgress(observer)
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPusher
|
||||
try {
|
||||
val uploadStream = context.contentResolver.openInputStream(localFile)
|
||||
syncthingClient.pushFile(uploadStream, syncthingFolder, syncthingPath, { observer ->
|
||||
onProgress(observer)
|
||||
try {
|
||||
while (!observer.isCompleted) {
|
||||
if (mCancelled)
|
||||
return@pushFile
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
|
||||
onProgress(observer)
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = " + observer.progressMessage)
|
||||
onProgress(observer)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
onError()
|
||||
}
|
||||
IOUtils.closeQuietly(uploadStream)
|
||||
|
||||
onComplete()
|
||||
}, { onError() })
|
||||
}, { this.onError() })
|
||||
} catch (e: IOException) {
|
||||
onError()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
isCancelled = true
|
||||
private fun createDialog() {
|
||||
mProgressDialog = ProgressDialog(context)
|
||||
mProgressDialog.setMessage(context.getString(R.string.dialog_uploading_file, fileName))
|
||||
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
mProgressDialog.setCancelable(true)
|
||||
mProgressDialog.setOnCancelListener { mCancelled = true }
|
||||
mProgressDialog.isIndeterminate = true
|
||||
mProgressDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
|
||||
doAsync {
|
||||
uiThread {
|
||||
mProgressDialog.isIndeterminate = false
|
||||
mProgressDialog.max = observer.dataSource.size.toInt()
|
||||
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete() {
|
||||
if (mCancelled)
|
||||
return
|
||||
|
||||
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
|
||||
doAsync {
|
||||
uiThread {
|
||||
mProgressDialog.dismiss()
|
||||
context.toast(R.string.toast_upload_complete)
|
||||
onUploadCompleteListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError() {
|
||||
doAsync {
|
||||
uiThread {
|
||||
mProgressDialog.dismiss()
|
||||
context.toast(R.string.toast_file_upload_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,18 @@ package net.syncthing.lite.utils
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import com.google.common.base.Objects.equal
|
||||
import com.google.common.base.Strings.nullToEmpty
|
||||
import org.apache.commons.lang3.StringUtils.capitalize
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.IOException
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.*
|
||||
import java.io.File
|
||||
|
||||
object Util {
|
||||
|
||||
fun getDeviceName(): String {
|
||||
val manufacturer = Build.MANUFACTURER ?: ""
|
||||
val model = Build.MODEL ?: ""
|
||||
val manufacturer = nullToEmpty(Build.MANUFACTURER)
|
||||
val model = nullToEmpty(Build.MODEL)
|
||||
val deviceName =
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
@@ -30,32 +24,17 @@ object Util {
|
||||
return deviceName ?: "android"
|
||||
}
|
||||
|
||||
fun getContentFileName(context: Context, uri: Uri): String {
|
||||
context.contentResolver.query(uri, null, null, null, null, null).use { cursor ->
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
throw InvalidParameterException("Cursor is null or empty")
|
||||
}
|
||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun importDeviceId(libraryHandler: LibraryHandler?, context: Context?, deviceId: String,
|
||||
onComplete: () -> Unit) {
|
||||
val deviceId2 = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
libraryHandler?.configuration { configuration ->
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
async(UI) {
|
||||
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
|
||||
onComplete()
|
||||
}
|
||||
} else {
|
||||
async(UI) {
|
||||
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
|
||||
}
|
||||
fun getContentFileName(context: Context, contentUri: Uri): String {
|
||||
var fileName = File(contentUri.lastPathSegment).name
|
||||
if (equal(contentUri.scheme, "content")) {
|
||||
context.contentResolver.query(contentUri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)!!.use { cursor ->
|
||||
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||
cursor.moveToFirst()
|
||||
val path = cursor.getString(columnIndex)
|
||||
Log.d("Main", "recovered 'content' uri real path = " + path)
|
||||
fileName = File(Uri.parse(path).lastPathSegment).name
|
||||
}
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#7D000000"
|
||||
android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#7D000000"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#7D000000"
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -6,6 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!--center content BEGIN-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -13,6 +14,36 @@
|
||||
android:divider="?android:listDivider"
|
||||
android:showDividers="middle">
|
||||
|
||||
<!--index loading progress BEGIN-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/index_update"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"/>
|
||||
<TextView
|
||||
android:id="@+id/index_update_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white_on_primary"
|
||||
android:text="@string/index_update_progress_message"
|
||||
android:layout_gravity="start"
|
||||
android:textAlignment="gravity"
|
||||
/>
|
||||
</LinearLayout>
|
||||
<!--index loading progress END-->
|
||||
|
||||
<!--main list view BEGIN-->
|
||||
<ListView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -38,9 +69,12 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
<!--main list view END-->
|
||||
|
||||
</LinearLayout>
|
||||
<!--center content END-->
|
||||
|
||||
<!--upload here overlay button BEGIN-->
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/main_list_view_upload_here_button"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -52,6 +86,7 @@
|
||||
app:elevation="6dp"
|
||||
app:pressedTranslationZ="12dp"
|
||||
android:src="@drawable/ic_file_upload_white_24dp"/>
|
||||
<!--upload here overlay button END-->
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/directory_empty" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -5,12 +5,40 @@
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<!-- The main content view -->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/index_update"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"/>
|
||||
<TextView
|
||||
android:id="@+id/index_update_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white_on_primary"
|
||||
android:text="@string/index_update_progress_message"
|
||||
android:layout_gravity="start"
|
||||
android:textAlignment="gravity"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- The navigation drawer -->
|
||||
<android.support.design.widget.NavigationView
|
||||
android:id="@+id/navigation"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -18,7 +46,6 @@
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
app:menu="@menu/drawer_view" />
|
||||
|
||||
</android.support.v4.widget.DrawerLayout>
|
||||
|
||||
</layout>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/device_id"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:clickable="true"
|
||||
android:drawableEnd="@drawable/ic_content_copy_black_24dp"
|
||||
android:focusable="true"
|
||||
android:fontFamily="monospace"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||
tools:text="ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="8dp"
|
||||
android:drawableEnd="@drawable/ic_share_black_24dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</layout>
|
||||
@@ -4,20 +4,27 @@
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="24dp"
|
||||
android:padding="@dimen/abc_action_bar_content_inset_material"
|
||||
android:theme="?alertDialogTheme"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp" />
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loading_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/abc_action_bar_content_inset_material" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loading_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</ListView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -23,14 +22,12 @@
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/add_device"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:useCompatPadding="true"
|
||||
android:src="@drawable/ic_add_white_24dp" />
|
||||
<uk.co.markormesher.android_fab.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:buttonIcon="@drawable/ic_add_white_24dp"
|
||||
app:buttonBackgroundColour="@color/accent"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/intro_primary"
|
||||
android:padding="28dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/intro_page_one_title"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:src="@mipmap/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#eee"
|
||||
android:text="@string/intro_page_one_description" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/intro_primary"
|
||||
android:padding="28dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/intro_page_three_title"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#eee"
|
||||
tools:text="@string/intro_page_three_description" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/intro_primary"
|
||||
android:padding="28dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/intro_page_two_title"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<include
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:id="@+id/enter_device_id"
|
||||
layout="@layout/view_enter_device_id" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#eee"
|
||||
android:text="@string/intro_page_two_description" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,13 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
</layout>
|
||||
@@ -5,8 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/device_id_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/device_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minLines="3"
|
||||
android:inputType="textNoSuggestions|textMultiLine|textCapCharacters"
|
||||
tools:text="VPNPKMK-VL2SOQN-SS5I2AB-G4BV7ZK-RO5ODEE-Y2G3CZ4-C4FUW4P-ZEMJOAF"/>
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/scan_qr_code"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:padding="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_qr_code_black_24dp"
|
||||
android:background="?android:selectableItemBackgroundBorderless"/>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -14,15 +14,10 @@
|
||||
android:title="@string/devices_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_settings_gray_24dp"
|
||||
android:title="@string/settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/device_id"
|
||||
android:icon="@drawable/ic_qr_code_black_24dp"
|
||||
android:title="@string/show_device_id"
|
||||
android:checkable="false" />
|
||||
android:id="@+id/update_index"
|
||||
android:icon="@drawable/ic_refresh_gray_24dp"
|
||||
android:title="@string/update_remote_index_label"
|
||||
android:checkable="false"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/clear_index"
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="index_update_progress_message">Index wird aktualisiert...</string>
|
||||
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
|
||||
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
|
||||
<string name="update_remote_index_label">Index aktualisieren</string>
|
||||
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
|
||||
<string name="toast_write_storage_permission_required">Schreibrechte werden für diese Funktion benötigt</string>
|
||||
<string name="scan_qr_code">QR code scannen</string>
|
||||
<string name="enter_device_id">Geräte ID eingeben</string>
|
||||
<string name="invalid_device_id">Ungültige Geräte ID</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
<string name="toast_index_update_successful">Index erfolgreich aktualisiert</string>
|
||||
<string name="toast_index_update_failed">Index update für %1$d Geräte fehlgeschlagen</string>
|
||||
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
|
||||
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
|
||||
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
|
||||
<string name="toast_file_upload_failed">Hochladen gescheitert</string>
|
||||
<string name="toast_upload_complete">Hochladen erfolgreich</string>
|
||||
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
|
||||
<string name="directory_empty">Ordner ist leer</string>
|
||||
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
|
||||
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
|
||||
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
|
||||
<string name="index_update_folder">Index aktualisierung, Ordner</string>
|
||||
<string name="index_update_percent_synchronized">% synchronisiert</string>
|
||||
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
|
||||
<string name="last_modified">- zuletzt modifiziert</string>
|
||||
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</string>
|
||||
<string name="last_modified_known">zuletzt modifiziert:</string>
|
||||
<string name="remove_device_title">Gerät entfernen:</string>
|
||||
<string name="device_import_success">Gerät %1$s erfolgreich importiert</string>
|
||||
<string name="device_already_known">Gerät ist bereits bekannt $1%s</string>
|
||||
<string name="remove_device_body_1">Gerät</string>
|
||||
<string name="remove_device_body_2">von den bekannten Geräten entfernen?</string>
|
||||
<string name="device_import_success">Gerät erfolgreich importiert:</string>
|
||||
<string name="device_already_known">Gerät ist bereits bekannt:</string>
|
||||
<string name="folders_label">Ordner</string>
|
||||
<string name="devices_label">Geräte</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d Dateien, %3$d Ordner</string>
|
||||
<string name="file_info">%1$s, zuletzt modifiziert %2$s</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -1,26 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Aucun dossier disponible</string>
|
||||
<string name="clear_local_cache_index_label">Effacer le cache et l\'index local</string>
|
||||
<string name="devices_list_view_empty_message">Aucun appareil disponible</string>
|
||||
<string name="scan_qr_code">Scanner le QR Code</string>
|
||||
<string name="enter_device_id">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="dialog_downloading_file">Téléchargement du fichier %1$s</string>
|
||||
<string name="toast_file_download_failed">Le téléchargement du fichier a échoué</string>
|
||||
<string name="toast_open_file_failed">Aucune appli compatible trouvée</string>
|
||||
<string name="toast_file_upload_failed">Échec de l\'upload</string>
|
||||
<string name="toast_upload_complete">Upload du fichier terminé</string>
|
||||
<string name="dialog_uploading_file">Upload du fichier %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Effacer le cache local et l\'index?</string>
|
||||
<string name="clear_cache_and_index_body">Effacer toutes les données du cache local et de l\'index ?</string>
|
||||
<string name="last_modified_time">Dernière modification : %1$s</string>
|
||||
<string name="remove_device_title">Supprimer l\'appareil %1$s\?</string>
|
||||
<string name="device_import_success">Appareil %1$s importé avec succès</string>
|
||||
<string name="device_already_known">Appareil déjà connu %1$s</string>
|
||||
<string name="folders_label">Dossiers</string>
|
||||
<string name="devices_label">Appareils</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d fichiers, %3$d dossiers</string>
|
||||
<string name="file_info">%1$s, dernière modification %2$s</string>
|
||||
</resources>
|
||||
@@ -1,27 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Nessuna cartella disponibile</string>
|
||||
<string name="clear_local_cache_index_label">Cancella cache/indice</string>
|
||||
<string name="devices_list_view_empty_message">Nessun dispositivo disponibile</string>
|
||||
<string name="scan_qr_code">Scansiona codice QR</string>
|
||||
<string name="enter_device_id">Inserisci ID dispositivo</string>
|
||||
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
|
||||
<string name="dialog_downloading_file">Download del file %1$s</string>
|
||||
<string name="toast_file_download_failed">Impossibile scaricare il file</string>
|
||||
<string name="toast_open_file_failed">Nessuna applicazione compatibile trovata</string>
|
||||
<string name="toast_file_upload_failed">Caricamento file fallito</string>
|
||||
<string name="toast_upload_complete">Caricamento file completato</string>
|
||||
<string name="dialog_uploading_file">Caricamento del file %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Cancellare la cache locale e l\'indice?</string>
|
||||
<string name="clear_cache_and_index_body">Cancellare tutti i dati della cache locale e i dati dell\'indice?</string>
|
||||
<string name="index_update_progress_label">Aggiornamento dell\'indice per la cartella %1$s, %2$d%% sincronizzato</string>
|
||||
<string name="last_modified_time">Ultima modifica: %1$s</string>
|
||||
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</string>
|
||||
<string name="device_import_success">Dispositivo %1$s importato con successo</string>
|
||||
<string name="device_already_known">Dispositivo %1$s già presente</string>
|
||||
<string name="folders_label">Cartelle</string>
|
||||
<string name="devices_label">Dispositivi</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d file, %3$d cartelle</string>
|
||||
<string name="file_info">%1$s, ultima modifica %2$s</string>
|
||||
</resources>
|
||||
@@ -1,27 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">フォルダーがありません</string>
|
||||
<string name="clear_local_cache_index_label">ローカルキャッシュ/索引をクリア</string>
|
||||
<string name="devices_list_view_empty_message">デバイスがありません</string>
|
||||
<string name="scan_qr_code">QR コードをスキャン</string>
|
||||
<string name="enter_device_id">デバイス ID を入力</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
<string name="dialog_downloading_file">ファイル %1$s のダウンロード中</string>
|
||||
<string name="toast_file_download_failed">ファイルのダウンロードに失敗しました</string>
|
||||
<string name="toast_open_file_failed">利用できるアプリが見つかりません</string>
|
||||
<string name="toast_file_upload_failed">ファイルのアップロードに失敗しました</string>
|
||||
<string name="toast_upload_complete">ファイルのアップロードが完了しました</string>
|
||||
<string name="dialog_uploading_file">ファイル %1$s のアップロード中</string>
|
||||
<string name="clear_cache_and_index_title">ローカルキャッシュと索引をクリアしますか?</string>
|
||||
<string name="clear_cache_and_index_body">すべてのローカルキャッシュデータと索引データをクリアしますか?</string>
|
||||
<string name="index_update_progress_label">フォルダー %1$s の索引を更新しました。 %2$d%% 同期しました</string>
|
||||
<string name="last_modified_time">最終更新: %1$s</string>
|
||||
<string name="remove_device_title">デバイス %1$sを削除しますか?</string>
|
||||
<string name="device_import_success">デバイス %1$s のインポートに成功しました</string>
|
||||
<string name="device_already_known">デバイスは既に存在します %1$s</string>
|
||||
<string name="folders_label">フォルダー</string>
|
||||
<string name="devices_label">デバイス</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d ファイル, %3$d ディレクトリー</string>
|
||||
<string name="file_info">%1$s, 最終更新 %2$s</string>
|
||||
</resources>
|
||||
@@ -1,26 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Nici un director disponibil</string>
|
||||
<string name="clear_local_cache_index_label">Curăță indexul/memoria locală</string>
|
||||
<string name="devices_list_view_empty_message">Nici un dispozitiv disponibil</string>
|
||||
<string name="scan_qr_code">Scanează cod QR</string>
|
||||
<string name="enter_device_id">Introduceți ID dispozitiv</string>
|
||||
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
|
||||
<string name="dialog_downloading_file">Se descarcă fișierul %1$s</string>
|
||||
<string name="toast_file_download_failed">Descărcarea fișierului a eșuat</string>
|
||||
<string name="toast_open_file_failed">Nu a fost găsită nici o aplicație compatibilă</string>
|
||||
<string name="toast_file_upload_failed">Încărcarea fișierului a eșuat</string>
|
||||
<string name="toast_upload_complete">Încărcarea fișierelor finalizată</string>
|
||||
<string name="dialog_uploading_file">Se încarcă fișierul %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Se curăță memoria locală și indexul?</string>
|
||||
<string name="clear_cache_and_index_body">Se curăță datele din memoria locală și datele indexului?</string>
|
||||
<string name="last_modified_time">Modificat ultima dată pe: %1$s</string>
|
||||
<string name="remove_device_title">Ștergere dispozitiv %1$s\?</string>
|
||||
<string name="device_import_success">Dispozitiv importat cu succes %1$s</string>
|
||||
<string name="device_already_known">Dispozitiv deja prezent %1$s</string>
|
||||
<string name="folders_label">Directoare</string>
|
||||
<string name="devices_label">Dispozitive</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d fișier(e), %3$d director(oare)</string>
|
||||
<string name="file_info">%1$s, modificat ultima dată pe %2$s</string>
|
||||
</resources>
|
||||
@@ -1,10 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#f43703</color>
|
||||
<color name="primary_light">#ff6f39</color>
|
||||
<color name="primary_dark">#b90000</color>
|
||||
<color name="primary_dark">#d13602</color>
|
||||
<color name="white_on_primary">#fefefe</color>
|
||||
<color name="accent">#FFC107</color>
|
||||
<color name="divider">#1F000000</color>
|
||||
<color name="intro_primary">#ff5252</color>
|
||||
<color name="intro_primary_dark">#c50e29</color>
|
||||
|
||||
<color name="device_online_active">#ff99cc00</color>
|
||||
<color name="device_online_inactive">#f43703</color>
|
||||
<color name="device_offline">#aaaaaa</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,46 +1,37 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="app_name" translatable="false">Syncthing Lite</string>
|
||||
<string name="index_update_progress_message">Index update…</string>
|
||||
<string name="folder_list_empty_message">No folder available</string>
|
||||
<string name="clear_local_cache_index_label">Clear local cache/index</string>
|
||||
<string name="update_remote_index_label">Update remote index</string>
|
||||
<string name="devices_list_view_empty_message">No devices available</string>
|
||||
<string name="toast_write_storage_permission_required">Write storage permission is required for this functionality</string>
|
||||
<string name="scan_qr_code">Scan QR code</string>
|
||||
<string name="enter_device_id">Enter device ID</string>
|
||||
<string name="invalid_device_id">Error: Invalid device ID</string>
|
||||
<string name="invalid_device_id">Invalid device ID</string>
|
||||
<string name="device_id_dialog_title">Enter Device ID</string>
|
||||
<string name="toast_index_update_successful">Index update successful</string>
|
||||
<string name="toast_index_update_failed">Index update failed for %1$d devices</string>
|
||||
<string name="dialog_downloading_file">Downloading file %1$s</string>
|
||||
<string name="toast_file_download_failed">Failed to download file</string>
|
||||
<string name="toast_open_file_failed">No compatible app found</string>
|
||||
<string name="toast_file_upload_failed">File upload failed</string>
|
||||
<string name="toast_upload_complete">File upload complete</string>
|
||||
<string name="dialog_uploading_file">Uploading file %1$s</string>
|
||||
<string name="directory_empty">Directory is empty</string>
|
||||
<string name="clear_cache_and_index_title">Clear local cache and index?</string>
|
||||
<string name="clear_cache_and_index_body">Clear all local cache data and index data?</string>
|
||||
<string name="index_update_progress_label">Index update for folder %1$s, %2$d%% synchronized</string>
|
||||
<string name="loading_config_starting_syncthing_client">Loading config, starting syncthing client…</string>
|
||||
<string name="last_modified_time">Last modified: %1$s</string>
|
||||
<string name="remove_device_title">Remove device %1$s\?</string>
|
||||
<string name="remove_device_message">Remove %1$s from the list of known devices?</string>
|
||||
<string name="device_import_success">Successfully imported device %1$s</string>
|
||||
<string name="device_already_known">Device already present %1$s</string>
|
||||
<string name="index_update_folder">Index update, folder</string>
|
||||
<string name="index_update_percent_synchronized">% synchronized</string>
|
||||
<string name="loading_config_starting_syncthing_client">Loading config, starting syncthing client</string>
|
||||
<string name="last_modified">- last modified</string>
|
||||
<string name="last_modified_unknown">last modified: unknown</string>
|
||||
<string name="last_modified_known">last modified:</string>
|
||||
<string name="remove_device_title">Remove device:</string>
|
||||
<string name="remove_device_body_1">Remove device</string>
|
||||
<string name="remove_device_body_2">from list of known devices?</string>
|
||||
<string name="device_import_success">Successfully imported device:</string>
|
||||
<string name="device_already_known">Device already present:</string>
|
||||
<string name="folders_label">Folders</string>
|
||||
<string name="devices_label">Devices</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d files, %3$d directories</string>
|
||||
<string name="file_info">%1$s, last modified %2$s</string>
|
||||
<string name="show_device_id">Show device ID</string>
|
||||
<string name="device_id">Device ID</string>
|
||||
<string name="device_id_copied">Device ID copied to clipboard</string>
|
||||
<string name="share_device_id_chooser">Share device ID with</string>
|
||||
<string name="other_syncthing_instance_title">Another Syncthing instance is running</string>
|
||||
<string name="other_syncthing_instance_message">Local discovery will not work. Stop the other Syncthing instance to enable local discovery.</string>
|
||||
<string name="intro_page_one_title">Welcome to Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing replaces proprietary sync and cloud services with something open, trustworthy and decentralized. Your data is your data alone and you deserve to choose where it is stored, if it is shared with some third party and how it\'s transmitted over the Internet.</string>
|
||||
<string name="intro_page_two_title">Add a device</string>
|
||||
<string name="intro_page_three_title">Share your folders</string>
|
||||
<string name="intro_page_two_description">Enter a Syncthing device ID, or scan a device ID from a QR code</string>
|
||||
<string name="intro_page_three_description">Now accept the device with ID %1$s, and share a folder with it. It may take a few minutes until the devices connect.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_app_version_title">App version</string>
|
||||
<string name="settings_local_device_name">Local device name</string>
|
||||
<string name="settings_local_device_summary">The name that other devices will see for this device</string>
|
||||
</resources>
|
||||
|
||||
@@ -4,15 +4,6 @@
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Syncthing.NoActionBar" parent="@style/Theme.AppCompat">
|
||||
<item name="colorPrimary">@color/intro_primary</item>
|
||||
<item name="colorPrimaryDark">@color/intro_primary_dark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="files" path="/" />
|
||||
</paths>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/settings">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="local_device_name"
|
||||
android:title="@string/settings_local_device_name"
|
||||
android:summary="@string/settings_local_device_summary"
|
||||
android:persistent="false"/>
|
||||
|
||||
<Preference
|
||||
android:key="app_version"
|
||||
android:title="@string/settings_app_version_title"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
+4
-3
@@ -1,10 +1,9 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.20'
|
||||
ext.kotlin_version = '1.2.0'
|
||||
ext.support_version = '27.0.2'
|
||||
ext.build_tools_version = '3.0.1'
|
||||
ext.anko_version = '0.10.4'
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
@@ -14,7 +13,9 @@ buildscript {
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$build_tools_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
NEW_VERSION_NAME=$1
|
||||
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}' | tr -d "\"")
|
||||
if [[ -z ${NEW_VERSION_NAME} ]]
|
||||
then
|
||||
echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Updating Translations
|
||||
-----------------------------
|
||||
"
|
||||
tx push -s
|
||||
# Force push/pull to make sure this is executed. Apparently tx only compares timestamps, not file
|
||||
# contents. So if a file was `touch`ed, it won't be updated by default.
|
||||
tx pull -a -f
|
||||
git add -A "app/src/main/res/values-*/strings.xml"
|
||||
if ! git diff --cached --exit-code;
|
||||
then
|
||||
git commit -m "Imported translations"
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Updating Version
|
||||
-----------------------------
|
||||
"
|
||||
OLD_VERSION_CODE=$(grep "versionCode" "app/build.gradle" -m 1 | awk '{print $2}')
|
||||
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
|
||||
sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" "app/build.gradle"
|
||||
sed -i "s/versionName \"$OLD_VERSION_NAME\"/versionName \"$NEW_VERSION_NAME\"/" "app/build.gradle"
|
||||
|
||||
LIBRARY_NAME="com.github.Nutomic:syncthing-java"
|
||||
sed -i "s/$LIBRARY_NAME:$OLD_VERSION_NAME/$LIBRARY_NAME:$NEW_VERSION_NAME/" "app/build.gradle"
|
||||
|
||||
git add "app/build.gradle"
|
||||
git commit -m "Version $NEW_VERSION_NAME"
|
||||
git tag ${NEW_VERSION_NAME}
|
||||
|
||||
echo "
|
||||
|
||||
Running Lint
|
||||
-----------------------------
|
||||
"
|
||||
./gradlew clean lintVitalRelease
|
||||
|
||||
echo "
|
||||
Update ready.
|
||||
"
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
version=$(git describe --tags)
|
||||
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
if [[ ! ${version} =~ $regex ]]
|
||||
then
|
||||
echo "Current commit is not a release"
|
||||
exit;
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Pushing to Github
|
||||
-----------------------------
|
||||
"
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
echo "
|
||||
|
||||
Push to Google Play
|
||||
-----------------------------
|
||||
"
|
||||
|
||||
read -s -p "Enter signing password: " password
|
||||
|
||||
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
|
||||
|
||||
# Upload apk and listing to Google Play
|
||||
SIGNING_PASSWORD=${password} ./gradlew publishRelease
|
||||
|
||||
echo "
|
||||
|
||||
Release published!
|
||||
"
|
||||
Reference in New Issue
Block a user