Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ec151534e | |||
| 1e97e11cb7 | |||
| 681fdb9802 | |||
| 0df347377a | |||
| d6fc356bd7 | |||
| 78ad8756d5 | |||
| b96c672a8e | |||
| 2a25e9882b | |||
| cb33d8f3e4 | |||
| db8e91eafa | |||
| f9b91f6ef8 | |||
| 01fb92e2c9 | |||
| 4b519e84e3 | |||
| f3d51f0cb9 | |||
| fa30beb9d5 | |||
| 919fdc31bd | |||
| b3f2af0ee7 | |||
| f33364939b | |||
| 1a773daf24 | |||
| b115a99907 | |||
| 5d09d011b1 | |||
| 71a433edf6 | |||
| 974817b7a3 | |||
| f00760bddd | |||
| 5f539c4149 | |||
| 1869a49c2c | |||
| 91289b05ce | |||
| 98bc67939f | |||
| fcb31ae9fa | |||
| 147ad6abcc | |||
| 4c13af3662 | |||
| 17f9ad336c | |||
| 852fc0d230 | |||
| 0032726e3e | |||
| c70211bc24 | |||
| a7f80fa45c | |||
| 461d64950b | |||
| c37832d084 | |||
| f336a2932f | |||
| 0b3e2bf914 | |||
| 6d9009daff | |||
| e2a246220e | |||
| 98d6656683 | |||
| c307953fce | |||
| 68f541f00b | |||
| 29c71f1ca9 | |||
| 76ddbdd3b4 | |||
| cae1026f35 | |||
| d07c934ea7 | |||
| d829c18e76 | |||
| e41ed80d05 | |||
| 3e691b61c0 | |||
| 0fb7a9e93d | |||
| 1b4205b04a | |||
| 8e00c8b4a0 | |||
| f3ca98be80 | |||
| 96fc8bfc7b | |||
| 58098aae0f | |||
| c4ad797905 | |||
| a61d8c5c4f | |||
| af579f8311 | |||
| fbdcdbf7ec | |||
| e6870a08d6 | |||
| fbee0ca0e8 | |||
| 65b42475a6 | |||
| af09b763a6 | |||
| 5680c6c554 | |||
| 2caaebfc33 | |||
| 460d421a79 | |||
| 96edbaa240 | |||
| de1566915b | |||
| a294d6f06c | |||
| 7ce017f48c | |||
| 55edc592a0 | |||
| eb5dfcbd46 | |||
| 2b55bd9e76 | |||
| eb8360f276 | |||
| 48f880ee4e | |||
| 9f5072ed3a | |||
| b1743db5af | |||
| f0b465f054 | |||
| 7bdcef8338 | |||
| c2823c1aad | |||
| 37b6a5492b | |||
| 9e632c2f58 | |||
| eaf948d3cd | |||
| 3242d3d742 | |||
| a90d724c06 | |||
| c257a730b0 | |||
| fcbd344491 | |||
| 773bb4259b | |||
| 3f6382e9c6 | |||
| 660346a856 | |||
| 93f2e0de57 | |||
| 1fbecc449a | |||
| c7d368dee6 | |||
| 5addeb8ea6 | |||
| 57364b4e14 | |||
| 4f9c44a4ad | |||
| 126bb507ba | |||
| f352303f6b | |||
| 53749eac5c | |||
| 34147063eb | |||
| edeb5ccb0d | |||
| 248ccc0606 | |||
| 726c9c974e | |||
| 087b0f4ec1 | |||
| a6cc38f4a1 | |||
| 96a4ec1738 | |||
| 42b77cab12 | |||
| a28e5d704d | |||
| b924b50ddc | |||
| 96503dd9c1 | |||
| c83504c155 | |||
| acea170854 |
+32
@@ -0,0 +1,32 @@
|
||||
sudo: required
|
||||
language: android
|
||||
jdk: oraclejdk8
|
||||
dist: trusty
|
||||
|
||||
# Install Android SDK
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-28.0.2
|
||||
- android-28
|
||||
- extra-android-m2repository
|
||||
|
||||
before_install:
|
||||
# Hack to accept Android licenses
|
||||
- yes | sdkmanager "platforms;android-27"
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
|
||||
# Cache gradle dependencies
|
||||
# https://docs.travis-ci.com/user/languages/android/#Caching
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
|
||||
script:
|
||||
- ./gradlew lint
|
||||
- ./gradlew assembleDebug
|
||||
@@ -1,40 +1,42 @@
|
||||
# Syncthing Lite
|
||||
|
||||
[](https://travis-ci.org/syncthing/syncthing-lite)
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
This project is an Android app, that works as a client for a [Syncthing][1] share (accessing
|
||||
Syncthing devices in the same way a client-server file sharing app access its proprietary server).
|
||||
Syncthing devices in the same way a client-server file sharing app accesses its proprietary server).
|
||||
|
||||
This is a client-oriented implementation, designed to work online by downloading and
|
||||
uploading files from an active device on the network (instead of synchronizing a local copy of
|
||||
the entire repository). This is quite different from the way the [syncthing-android][2] works,
|
||||
and its useful from those devices that cannot or wish not to download the entire repository (for
|
||||
the entire repository).
|
||||
Due to that, you will see a sync progress of 0% at other devices (and this is expected).
|
||||
This is quite different from the way the [syncthing-android][2] works,
|
||||
and it's useful for those devices that cannot or do not wish to download the entire repository (for
|
||||
example, mobile devices with limited storage available, wishing to access a syncthing share).
|
||||
|
||||
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
|
||||
This project is based on syncthing-java (which is in this repository too), a java implementation of Syncthing protocols.
|
||||
|
||||
Due to the behaviour of this App and the [behaviour of the Syncthing Server](https://github.com/syncthing/syncthing/issues/5224),
|
||||
you can't reconnect for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
|
||||
This does not apply to local discovery connections.
|
||||
|
||||
[<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/).
|
||||
The project is translated on [Transifex](https://www.transifex.com/syncthing/syncthing-lite/).
|
||||
Requests for new languages are always accepted (but this happens manually because there is no option to accept it automatically).
|
||||
|
||||
## Building
|
||||
|
||||
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
|
||||
you install [Android Studio][4] and import the project.
|
||||
|
||||
To compile with a development version of the [syncthing-java][3] library, you have to install it to
|
||||
the local maven repository. To do this, clone the repo and run `gradle install` in the
|
||||
syncthing-java project folder.
|
||||
The project uses a standard Android build, and requires the Android SDK. The easiest option is to
|
||||
install [Android Studio][3] and import the project.
|
||||
|
||||
## License
|
||||
All code is licensed under the [MPLv2 License][5].
|
||||
All code is licensed under the [MPLv2 License][4].
|
||||
|
||||
[1]: https://syncthing.net/
|
||||
[2]: https://github.com/syncthing/syncthing-android
|
||||
[3]: https://github.com/Nutomic/syncthing-java
|
||||
[4]: https://developer.android.com/studio/index.html
|
||||
[5]: LICENSE
|
||||
[3]: https://developer.android.com/studio/index.html
|
||||
[4]: LICENSE
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Releasing
|
||||
|
||||
- do tests
|
||||
- update translations using ``tx pull -af`` (as extra merge request or branch for the case it does not build correctly)
|
||||
- update the version name and version code of the app [here](https://github.com/syncthing/syncthing-lite/blob/master/app/build.gradle)
|
||||
- update the changelog at [app/src/main/play/en-GB/whatsnew](https://github.com/syncthing/syncthing-lite/blob/master/app/src/main/play/en-GB/whatsnew)
|
||||
- create a tag/ release in GitHub with an changelog; The tag name should be the version number
|
||||
- trigger a release at <https://build.syncthing.net/> to publish the release to google play
|
||||
- F-Droid picks up the release by the tag
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Roadmap
|
||||
|
||||
## What should happen
|
||||
|
||||
- fixing bugs and crashs
|
||||
- just create a issue WITH a detailed crash report (not: it does not work)
|
||||
- search if there is an other issue for it before creating a new one
|
||||
- add option to manually select the IP address of an device (<https://github.com/syncthing/syncthing-lite/issues/25>)
|
||||
- allow custom discovery servers or disabling device discovery (<https://github.com/syncthing/syncthing-lite/issues/105>)
|
||||
- downloading all files of an folder (<https://github.com/syncthing/syncthing-lite/issues/34>)
|
||||
- better server offline handling (<https://github.com/syncthing/syncthing-lite/issues/63>)
|
||||
- file uploading support (it currently does not work) <https://github.com/syncthing/syncthing-lite/issues/70>
|
||||
|
||||
## What could happen
|
||||
|
||||
- thumbnails (<https://github.com/syncthing/syncthing-lite/issues/37>)
|
||||
|
||||
## What will not happen
|
||||
|
||||
- additional encryption within the App (see <https://github.com/syncthing/syncthing-lite/issues/85>)
|
||||
+41
-17
@@ -1,19 +1,27 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'com.github.triplet.play'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "27.0.2"
|
||||
dataBinding.enabled = true
|
||||
|
||||
playAccountConfigs {
|
||||
defaultAccountConfig {
|
||||
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 7
|
||||
versionName "0.1.5"
|
||||
targetSdkVersion 26
|
||||
versionCode 21
|
||||
versionName "0.3.11"
|
||||
multiDexEnabled true
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
@@ -34,35 +42,51 @@ android {
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/*'
|
||||
}
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
play {
|
||||
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
|
||||
uploadImages = true
|
||||
track = 'production'
|
||||
}
|
||||
|
||||
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"
|
||||
kapt "com.android.databinding:compiler:$build_tools_version"
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:cardview-v7:$support_version"
|
||||
implementation ("com.github.Nutomic:syncthing-java:0.1.5") {
|
||||
exclude group: 'commons-logging', module:'commons-logging'
|
||||
exclude group: 'org.apache.httpcomponents', module:'httpclient'
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
implementation 'android.arch.lifecycle:extensions:1.1.1'
|
||||
|
||||
implementation(project(':syncthing-client')) {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
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 ('uk.co.markormesher:android-fab:2.0.0') {
|
||||
exclude group: "org.jetbrains.kotlin"
|
||||
}
|
||||
implementation 'com.google.zxing:core:3.3.0'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.3'
|
||||
|
||||
implementation project(':syncthing-repository-android')
|
||||
implementation project(':syncthing-temp-repository-encryption')
|
||||
}
|
||||
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /home/jonas/android-studio/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# ensure that stack traces make sense
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# this library uses factories with reflection
|
||||
-keep class net.jpountz.lz4.** { *; }
|
||||
|
||||
# from https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/kotlinx-coroutines-android/example-app/app/proguard-rules.pro
|
||||
# kotlin coroutines crash without it
|
||||
-keepclassmembernames class kotlinx.** {
|
||||
volatile <fields>;
|
||||
}
|
||||
|
||||
# fix detecting the main dispatcher
|
||||
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
||||
|
||||
# disable warnings
|
||||
-dontwarn com.google.protobuf.UnsafeUtil
|
||||
-dontwarn com.google.protobuf.UnsafeUtil$1
|
||||
-dontwarn net.jpountz.util.UnsafeUtils
|
||||
-dontwarn org.bouncycastle.cert.dane.fetcher.JndiDANEFetcherFactory
|
||||
-dontwarn org.bouncycastle.cert.dane.fetcher.JndiDANEFetcherFactory$1
|
||||
-dontwarn org.bouncycastle.jce.provider.X509LDAPCertStoreSpi
|
||||
-dontwarn org.bouncycastle.mail.smime.CMSProcessableBodyPart
|
||||
-dontwarn org.bouncycastle.mail.smime.CMSProcessableBodyPartInbound
|
||||
-dontwarn org.bouncycastle.mail.smime.CMSProcessableBodyPartOutbound
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateCompressedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateEncryptedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateLargeCompressedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateLargeEncryptedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateLargeSignedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateSignedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.CreateSignedMultipartMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ExampleUtils
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ReadCompressedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ReadEncryptedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ReadLargeCompressedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ReadLargeEncryptedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ReadLargeSignedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ReadSignedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.SendSignedAndEncryptedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.examples.ValidateSignedMail
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.multipart_signed
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.multipart_signed$LineOutputStream
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.PKCS7ContentHandler
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.pkcs7_mime
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.pkcs7_signature
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.x_pkcs7_mime
|
||||
-dontwarn org.bouncycastle.mail.smime.handlers.x_pkcs7_signature
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMECompressed
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedGenerator
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedGenerator$1
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedGenerator$ContentCompressor
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedParser
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEEnveloped
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator$1
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator$ContentEncryptor
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedParser
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEGenerator
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESigned
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESigned$1
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESignedGenerator
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESignedGenerator$1
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESignedGenerator$ContentSigner
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESignedParser
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMESignedParser$1
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEToolkit
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEUtil
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEUtil$LineOutputStream
|
||||
-dontwarn org.bouncycastle.mail.smime.SMIMEUtil$WriteOnceFileBackedMimeBodyPart
|
||||
-dontwarn org.bouncycastle.mail.smime.util.FileBackedMimeBodyPart
|
||||
-dontwarn org.bouncycastle.mail.smime.util.SharedFileInputStream
|
||||
-dontwarn org.bouncycastle.mail.smime.validator.SignedMailValidator
|
||||
-dontwarn org.bouncycastle.x509.util.LDAPStoreHelper
|
||||
@@ -4,6 +4,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:name=".android.Application"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -17,17 +18,15 @@
|
||||
<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:name=".library.CacheFileProvider"
|
||||
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>
|
||||
android:exported="false" />
|
||||
<provider
|
||||
android:name=".library.SyncthingProvider"
|
||||
android:authorities="net.syncthing.lite.documents"
|
||||
|
||||
@@ -4,129 +4,208 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import net.syncthing.java.bep.index.browser.DirectoryContentListing
|
||||
import net.syncthing.java.bep.index.browser.DirectoryListing
|
||||
import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
|
||||
import net.syncthing.java.bep.index.browser.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.FolderContentsAdapter
|
||||
import net.syncthing.lite.adapters.FolderContentsListener
|
||||
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
|
||||
import net.syncthing.lite.utils.FileDownloadDialog
|
||||
import net.syncthing.lite.utils.FileUploadDialog
|
||||
import net.syncthing.lite.dialogs.EnableFolderSyncForNewDeviceDialog
|
||||
import net.syncthing.lite.dialogs.FileMenuDialogFragment
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
import net.syncthing.lite.dialogs.ReconnectIssueDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
|
||||
class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FolderBrowserActivity"
|
||||
private const val REQUEST_SELECT_UPLOAD_FILE = 171
|
||||
|
||||
private const val STATUS_PATH = "path"
|
||||
const val EXTRA_FOLDER_NAME = "folder_name"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityFolderBrowserBinding
|
||||
private lateinit var indexBrowser: IndexBrowser
|
||||
private lateinit var adapter: FolderContentsAdapter
|
||||
private lateinit var folder: String
|
||||
|
||||
private val path = ConflatedBroadcastChannel<String>()
|
||||
private val listing = ConflatedBroadcastChannel<DirectoryListing?>(null)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
|
||||
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
|
||||
adapter = FolderContentsAdapter(this)
|
||||
|
||||
val binding: ActivityFolderBrowserBinding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
|
||||
val adapter = FolderContentsAdapter()
|
||||
|
||||
binding.listView.adapter = adapter
|
||||
binding.listView.setOnItemClickListener { _, _, position, _ ->
|
||||
val fileInfo = binding.listView.getItemAtPosition(position) as FileInfo
|
||||
navigateToFolder(fileInfo)
|
||||
binding.mainListViewUploadHereButton.setOnClickListener {
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
},
|
||||
REQUEST_SELECT_UPLOAD_FILE
|
||||
)
|
||||
}
|
||||
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
|
||||
libraryHandler?.syncthingClient {
|
||||
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
|
||||
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
|
||||
adapter.listener = object: FolderContentsListener {
|
||||
override fun onItemClicked(fileInfo: FileInfo) {
|
||||
if (fileInfo.isDirectory()) {
|
||||
path.offer(fileInfo.path)
|
||||
} else {
|
||||
DownloadFileDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClicked(fileInfo: FileInfo): Boolean {
|
||||
return if (fileInfo.type == FileInfo.FileType.FILE) {
|
||||
FileMenuDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReconnectIssueDialogFragment.showIfNeeded(this)
|
||||
|
||||
folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
|
||||
path.offer(if (savedInstanceState == null) IndexBrowser.ROOT_PATH else savedInstanceState.getString(STATUS_PATH))
|
||||
|
||||
launch {
|
||||
var job = Job()
|
||||
|
||||
path.consumeEach { path ->
|
||||
job.cancel()
|
||||
job = Job()
|
||||
|
||||
binding.listView.scrollToPosition(0)
|
||||
|
||||
listing.send(null)
|
||||
|
||||
async(job) {
|
||||
libraryHandler.libraryManager.streamDirectoryListing(folder, path).consumeEach {
|
||||
listing.send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
listing.openSubscription().consumeEach { listing ->
|
||||
if (listing == null) {
|
||||
binding.isLoading = true
|
||||
} else {
|
||||
supportActionBar?.title = if (PathUtils.isRoot(listing.path)) folder else PathUtils.getFileName(listing.path)
|
||||
binding.isLoading = false
|
||||
adapter.data = if (listing is DirectoryContentListing)
|
||||
listing.entries.sortedWith(IndexBrowser.sortAlphabeticallyDirectoriesFirst)
|
||||
else
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
launch {
|
||||
val devicesToAskFor = libraryHandler.libraryManager.withLibrary {
|
||||
val folderInfo = it.configuration.folders.find { it.folderId == folder }
|
||||
val notIgnoredBlacklistEntries = folderInfo?.notIgnoredBlacklistEntries ?: emptySet()
|
||||
|
||||
notIgnoredBlacklistEntries.mapNotNull { deviceId ->
|
||||
it.configuration.peers.find { peer -> peer.deviceId == deviceId }
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesToAskFor.isNotEmpty()) {
|
||||
EnableFolderSyncForNewDeviceDialog.newInstance(
|
||||
folderId = folder,
|
||||
devices = devicesToAskFor,
|
||||
folderName = libraryHandler.libraryManager.withLibrary {
|
||||
it.configuration.folders.find { it.folderId == folder }?.label ?: folder
|
||||
}
|
||||
).show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Thread {
|
||||
indexBrowser.setOnFolderChangedListener(null)
|
||||
indexBrowser.close()
|
||||
}.start()
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putString(STATUS_PATH, path.value)
|
||||
}
|
||||
|
||||
private fun goUp(): Boolean {
|
||||
val currentListing = listing.value
|
||||
|
||||
val parentPath = when (currentListing) {
|
||||
is DirectoryContentListing -> currentListing.parentEntry?.path
|
||||
is DirectoryNotFoundListing -> currentListing.theoreticalParentPath
|
||||
else -> null
|
||||
}
|
||||
|
||||
return if (parentPath == null) {
|
||||
false
|
||||
} else {
|
||||
path.offer(parentPath)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val listView = binding.listView
|
||||
//click item '0', ie '..' (go to parent)
|
||||
listView.performItemClick(adapter.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
|
||||
if (!goUp()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
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) } )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFolderListView(path: String) {
|
||||
indexBrowser.navigateToNearestPath(path)
|
||||
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)) {
|
||||
finish()
|
||||
} else {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFolderChanged() {
|
||||
runOnUiThread {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.listView.visibility = View.VISIBLE
|
||||
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
|
||||
adapter.clear()
|
||||
adapter.addAll(list)
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.listView.setSelection(0)
|
||||
if (indexBrowser.isRoot())
|
||||
libraryHandler?.folderBrowser {
|
||||
supportActionBar?.title = it.getFolderInfo(indexBrowser.folder)?.label
|
||||
libraryHandler.syncthingClient { syncthingClient ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
// FIXME: it would be better if the dialog would use the library handler
|
||||
FileUploadDialog(
|
||||
this@FolderBrowserActivity,
|
||||
syncthingClient,
|
||||
intent!!.data,
|
||||
folder,
|
||||
path.value,
|
||||
{ /* nothing to do on success */ }
|
||||
).show()
|
||||
}
|
||||
else
|
||||
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFolderListView() {
|
||||
showFolderListView(indexBrowser.currentPath)
|
||||
}
|
||||
|
||||
private fun showUploadHereDialog() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "*/*"
|
||||
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.folder_browser, menu)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
super.onIndexUpdateComplete(folderInfo)
|
||||
updateFolderListView()
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.go_home -> {
|
||||
finish()
|
||||
|
||||
true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
if (!goUp()) {
|
||||
finish()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.arch.lifecycle.Observer
|
||||
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 android.widget.Button
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
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)
|
||||
|
||||
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 onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
library.configuration.update { oldConfig ->
|
||||
oldConfig.copy(localDeviceName = Util.getDeviceName())
|
||||
}
|
||||
|
||||
library.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentIntroOneBinding.inflate(inflater, container, false)
|
||||
|
||||
libraryHandler.isListeningPortTaken.observe(this, Observer { binding.listeningPortTaken = it })
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display device ID entry field and QR scanner option.
|
||||
*/
|
||||
class IntroFragmentTwo : SyncthingFragment(), ISlidePolicy {
|
||||
|
||||
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.libraryManager, context!!, deviceId, { })
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
binding.enterDeviceId.deviceId.error = getString(R.string.invalid_device_id)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPolicyRespected() = isDeviceIdValid()
|
||||
|
||||
override fun onUserIllegallyRequestedNextPage() {
|
||||
// nothing to do, but some user feedback would be nice
|
||||
}
|
||||
|
||||
private val addedDeviceIds = HashSet<DeviceId>()
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
binding.foundDevices.removeAllViews()
|
||||
addedDeviceIds.clear()
|
||||
|
||||
libraryHandler.registerMessageFromUnknownDeviceListener(onDeviceFound)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
libraryHandler.unregisterMessageFromUnknownDeviceListener(onDeviceFound)
|
||||
}
|
||||
|
||||
private val onDeviceFound: (DeviceId) -> Unit = {
|
||||
deviceId ->
|
||||
|
||||
if (addedDeviceIds.add(deviceId)) {
|
||||
binding.foundDevices.addView(
|
||||
Button(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
text = deviceId.deviceId
|
||||
|
||||
setOnClickListener {
|
||||
binding.enterDeviceId.deviceId.setText(deviceId.deviceId)
|
||||
binding.enterDeviceId.deviceIdHolder.isErrorEnabled = false
|
||||
|
||||
binding.scroll.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until remote device connects with new folder.
|
||||
*/
|
||||
class IntroFragmentThree : SyncthingFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentIntroThreeBinding.inflate(inflater, container, false)
|
||||
|
||||
launch {
|
||||
val ownDeviceId = libraryHandler.libraryManager.withLibrary { it.configuration.localDeviceId }
|
||||
|
||||
libraryHandler.subscribeToConnectionStatus().consumeEach {
|
||||
if (it.values.find { it.addresses.isNotEmpty() } != null) {
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$ownDeviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
} else {
|
||||
binding.description.text = getString(R.string.intro_page_three_searching_device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
libraryHandler.subscribeToFolderStatusList().consumeEach {
|
||||
if (it.isNotEmpty()) {
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,39 @@ 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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ActivityMainBinding
|
||||
import net.syncthing.lite.dialogs.DeviceIdDialogFragment
|
||||
import net.syncthing.lite.fragments.DevicesFragment
|
||||
import net.syncthing.lite.fragments.FoldersFragment
|
||||
import net.syncthing.lite.fragments.SyncthingFragment
|
||||
import net.syncthing.lite.fragments.SettingsFragment
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
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(
|
||||
@@ -67,6 +80,8 @@ 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 -> DeviceIdDialogFragment().show(supportFragmentManager)
|
||||
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))
|
||||
@@ -79,8 +94,7 @@ class MainActivity : SyncthingActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setContentFragment(fragment: SyncthingFragment) {
|
||||
currentFragment = fragment
|
||||
private fun setContentFragment(fragment: Fragment) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.content_frame, fragment)
|
||||
@@ -88,9 +102,10 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
it.syncthingClient.clearCacheAndIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,64 +4,57 @@ import android.app.AlertDialog
|
||||
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.core.beans.FolderInfo
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.async.CoroutineActivity
|
||||
import net.syncthing.lite.databinding.DialogLoadingBinding
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.jetbrains.anko.contentView
|
||||
import org.slf4j.impl.HandroidLoggerAdapter
|
||||
|
||||
abstract class SyncthingActivity : AppCompatActivity() {
|
||||
|
||||
var libraryHandler: LibraryHandler? = null
|
||||
private set
|
||||
abstract class SyncthingActivity : CoroutineActivity() {
|
||||
val libraryHandler: LibraryHandler by lazy {
|
||||
LibraryHandler(
|
||||
context = this@SyncthingActivity
|
||||
)
|
||||
}
|
||||
private var loadingDialog: AlertDialog? = null
|
||||
private var snackBar: Snackbar? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
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)
|
||||
|
||||
libraryHandler.start {
|
||||
if (!isDestroyed) {
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
|
||||
onLibraryLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
libraryHandler?.close()
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
libraryHandler.stop()
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
if (!isDestroyed) {
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
onLibraryLoaded()
|
||||
open fun onLibraryLoaded() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
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 onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
snackBar?.dismiss()
|
||||
snackBar = null
|
||||
}
|
||||
|
||||
open fun onLibraryLoaded() {}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
package net.syncthing.lite.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionStatus
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewDeviceBinding
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class DevicesAdapter(context: Context) :
|
||||
ArrayAdapter<DeviceInfo>(context, R.layout.listview_device, mutableListOf()) {
|
||||
class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
var data: List<Pair<DeviceInfo, ConnectionInfo>> by Delegates.observable(listOf()) {
|
||||
_, _, _ -> notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewDeviceBinding
|
||||
= if (v == null) {
|
||||
DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.listview_device, parent, false)
|
||||
} else {
|
||||
DataBindingUtil.bind(v)
|
||||
}
|
||||
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
|
||||
}
|
||||
binding.deviceIcon.setImageResource(icon)
|
||||
return binding.root
|
||||
var listener: DeviceAdapterListener? = null
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
override fun getItemId(position: Int) = data[position].first.deviceId.deviceId.hashCode().toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DeviceViewHolder(
|
||||
ListviewDeviceBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val context = binding.root.context
|
||||
val (deviceInfo, connectionInfo) = data[position]
|
||||
|
||||
binding.name = deviceInfo.name
|
||||
binding.isConnected = connectionInfo.status == ConnectionStatus.Connected
|
||||
|
||||
binding.status = when (connectionInfo.status) {
|
||||
ConnectionStatus.Connected -> context.getString(R.string.device_status_connected, connectionInfo.currentAddress?.address)
|
||||
ConnectionStatus.Connecting -> context.getString(R.string.device_status_connecting, connectionInfo.currentAddress?.address)
|
||||
ConnectionStatus.Disconnected -> if (connectionInfo.addresses.isEmpty())
|
||||
context.getString(R.string.device_status_no_address)
|
||||
else
|
||||
context.getString(R.string.device_status_disconnected, connectionInfo.addresses.size)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceInfo) ?: false }
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
interface DeviceAdapterListener {
|
||||
fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean
|
||||
}
|
||||
|
||||
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -1,39 +1,69 @@
|
||||
package net.syncthing.lite.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewFileBinding
|
||||
import org.apache.commons.io.FileUtils
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class FolderContentsAdapter(context: Context) :
|
||||
ArrayAdapter<FileInfo>(context, R.layout.listview_file, mutableListOf()) {
|
||||
// TODO: enable setHasStableIds and add a good way to get an id
|
||||
class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
|
||||
var data: List<FileInfo> by Delegates.observable(listOf()) {
|
||||
_, _, _ -> notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var listener: FolderContentsListener? = null
|
||||
|
||||
init {
|
||||
// setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderContentsViewHolder(
|
||||
ListviewFileBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: FolderContentsViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val fileInfo = data[position]
|
||||
|
||||
binding.fileName = fileInfo.fileName
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewFileBinding =
|
||||
if (v == null) {
|
||||
DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.listview_file, parent, false)
|
||||
} else {
|
||||
DataBindingUtil.bind(v)
|
||||
}
|
||||
val fileInfo = getItem(position)
|
||||
binding.fileLabel.text = fileInfo!!.fileName
|
||||
if (fileInfo.isDirectory()) {
|
||||
binding.fileIcon.setImageResource(R.drawable.ic_folder_black_24dp)
|
||||
binding.fileSize.visibility = View.GONE
|
||||
binding.fileSize = null
|
||||
} else {
|
||||
binding.fileIcon.setImageResource(R.drawable.ic_image_black_24dp)
|
||||
binding.fileSize.visibility = View.VISIBLE
|
||||
binding.fileSize.text = context.getString(R.string.file_info,
|
||||
binding.fileSize = binding.root.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))
|
||||
DateUtils.getRelativeDateTimeString(binding.root.context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
}
|
||||
return binding.root
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
listener?.onItemClicked(fileInfo)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
listener?.onItemLongClicked(fileInfo) ?: false
|
||||
}
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
// override fun getItemId(position: Int) = data[position].fileName.hashCode().toLong()
|
||||
}
|
||||
|
||||
interface FolderContentsListener {
|
||||
fun onItemClicked(fileInfo: FileInfo)
|
||||
fun onItemLongClicked(fileInfo: FileInfo): Boolean
|
||||
}
|
||||
|
||||
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -1,35 +1,70 @@
|
||||
package net.syncthing.lite.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import net.syncthing.java.bep.folder.FolderStatus
|
||||
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 kotlin.properties.Delegates
|
||||
|
||||
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
|
||||
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewFolderBinding =
|
||||
if (v == null) {
|
||||
DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.listview_folder, parent, false)
|
||||
} 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)
|
||||
return binding.root
|
||||
class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
|
||||
var data: List<FolderStatus> by Delegates.observable(listOf()) {
|
||||
_, _, _ -> notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var listener: FolderListAdapterListener? = null
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
override fun getItemId(position: Int) = data[position].info.folderId.hashCode().toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderListViewHolder (
|
||||
ListviewFolderBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: FolderListViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val item = data[position]
|
||||
val (folderInfo, folderStats) = item
|
||||
val context = holder.itemView.context
|
||||
|
||||
Log.d("FolderListAdapter", "$item")
|
||||
|
||||
binding.folderName = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
|
||||
|
||||
binding.lastModification = context.getString(R.string.last_modified_time,
|
||||
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
|
||||
binding.info = context.getString(R.string.folder_content_info, folderStats.sizeDescription, folderStats.fileCount, folderStats.dirCount)
|
||||
|
||||
binding.info2 = if (item.missingIndexUpdates == 0L)
|
||||
null
|
||||
else
|
||||
context.getString(R.string.pending_index_updates, item.missingIndexUpdates)
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
listener?.onFolderClicked(folderInfo, folderStats)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
listener?.onFolderLongClicked(folderInfo) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FolderListViewHolder(val binding: ListviewFolderBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
interface FolderListAdapterListener {
|
||||
fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats)
|
||||
fun onFolderLongClicked(folderInfo: FolderInfo): Boolean
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.syncthing.lite.android
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
|
||||
class Application: Application() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "Application"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
val mainThread = Thread.currentThread()
|
||||
|
||||
if (defaultHandler == null) {
|
||||
Log.w(LOG_TAG, "could not get default crash handler")
|
||||
}
|
||||
|
||||
fun handleCrash(ex: Throwable) {
|
||||
Log.w(LOG_TAG, "app crashed", ex)
|
||||
|
||||
ErrorStorage.reportError(
|
||||
this,
|
||||
Log.getStackTraceString(ex)
|
||||
)
|
||||
|
||||
if (defaultHandler != null) {
|
||||
defaultHandler.uncaughtException(mainThread, ex)
|
||||
} else {
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, ex ->
|
||||
if (Looper.getMainLooper() === Looper.myLooper()) {
|
||||
handleCrash(ex)
|
||||
} else {
|
||||
handler.post { handleCrash(ex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.syncthing.lite.async
|
||||
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class CoroutineActivity: AppCompatActivity(), CoroutineScope {
|
||||
val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.syncthing.lite.async
|
||||
|
||||
import android.support.v4.app.DialogFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class CoroutineDialogFragment: DialogFragment(), CoroutineScope {
|
||||
val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.syncthing.lite.async
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class CoroutineFragment: Fragment(), CoroutineScope {
|
||||
val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogDeviceIdBinding
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
class DeviceIdDialogFragment: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
private const val QR_RESOLUTION = 512
|
||||
private const val TAG = "DeviceIdDialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
|
||||
// use an placeholder to prevent size changes; this string is never shown
|
||||
binding.deviceId.text = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"
|
||||
binding.deviceId.visibility = View.INVISIBLE
|
||||
|
||||
binding.qrCode.setImageBitmap(Bitmap.createBitmap(QR_RESOLUTION, QR_RESOLUTION, Bitmap.Config.RGB_565))
|
||||
|
||||
libraryHandler.library { configuration, _, _ ->
|
||||
val deviceId = configuration.localDeviceId
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fun shareDeviceId() {
|
||||
context!!.startActivity(Intent.createChooser(
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, deviceId.deviceId)
|
||||
},
|
||||
context!!.getString(R.string.share_device_id_chooser)
|
||||
))
|
||||
}
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
binding.deviceId.text = deviceId.deviceId
|
||||
binding.deviceId.visibility = View.VISIBLE
|
||||
|
||||
binding.deviceId.setOnClickListener { copyDeviceId() }
|
||||
binding.share.setOnClickListener { shareDeviceId() }
|
||||
}
|
||||
|
||||
doAsync {
|
||||
val writer = QRCodeWriter()
|
||||
try {
|
||||
val bitMatrix = writer.encode(deviceId.deviceId, BarcodeFormat.QR_CODE, QR_RESOLUTION, QR_RESOLUTION)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
binding.flipper.displayedChild = 1
|
||||
binding.qrCode.setImageBitmap(bmp)
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(context!!, theme)
|
||||
.setTitle(context!!.getString(R.string.device_id))
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
fun show(manager: FragmentManager?) {
|
||||
super.show(manager, TAG)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
|
||||
class EnableFolderSyncForNewDeviceDialog: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val FOLDER_NAME = "folderName"
|
||||
private const val DEVICES = "devices"
|
||||
private const val STATUS_CURRENT_DEVICE_ID = "currentDeviceId"
|
||||
|
||||
private const val TAG = "EnableFolderSyncForNewDeviceDialog"
|
||||
|
||||
fun newInstance(folderId: String, folderName: String, devices: List<DeviceInfo>) = EnableFolderSyncForNewDeviceDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(FOLDER_ID, folderId)
|
||||
putString(FOLDER_NAME, folderName)
|
||||
putSerializable(DEVICES, ArrayList<DeviceInfo>(devices))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentDeviceId = 0
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val folderId = arguments!!.getString(FOLDER_ID)
|
||||
val folderName = arguments!!.getString(FOLDER_NAME)
|
||||
val devices = arguments!!.getSerializable(DEVICES) as ArrayList<DeviceInfo>
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
currentDeviceId = savedInstanceState.getInt(STATUS_CURRENT_DEVICE_ID)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.dialog_enable_folder_sync_for_new_device_title)
|
||||
.setMessage(R.string.dialog_enable_folder_sync_for_new_device_text)
|
||||
.setPositiveButton(R.string.dialog_enable_folder_sync_for_new_device_positive, null)
|
||||
.setNegativeButton(R.string.dialog_enable_folder_sync_for_new_device_negative, null)
|
||||
.create()
|
||||
|
||||
fun bindDeviceId() {
|
||||
if (currentDeviceId >= devices.size) {
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
val device = devices[currentDeviceId]
|
||||
|
||||
dialog.setMessage(getString(
|
||||
R.string.dialog_enable_folder_sync_for_new_device_text,
|
||||
folderName,
|
||||
device.name,
|
||||
device.deviceId.deviceId
|
||||
))
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
|
||||
|
||||
it.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
|
||||
oldFolderEntry.copy(
|
||||
deviceIdWhitelist = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId),
|
||||
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist - setOf(device.deviceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.syncthingClient.reconnect(device.deviceId)
|
||||
it.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
currentDeviceId++
|
||||
bindDeviceId()
|
||||
}
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
|
||||
|
||||
it.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
|
||||
oldFolderEntry.copy(
|
||||
ignoredDeviceIdList = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.syncthingClient.reconnect(device.deviceId)
|
||||
it.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
currentDeviceId++
|
||||
bindDeviceId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.setOnShowListener { bindDeviceId() }
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(STATUS_CURRENT_DEVICE_ID, currentDeviceId)
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.widget.Toast
|
||||
import net.syncthing.lite.R
|
||||
|
||||
class ErrorReportDialog: DialogFragment() {
|
||||
companion object {
|
||||
private const val REPORT = "report"
|
||||
private const val TAG = "ErrorReportDialog"
|
||||
|
||||
fun newInstance(report: String) = ErrorReportDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(REPORT, report)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val report = arguments!!.getString(REPORT)
|
||||
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
return AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.settings_last_error_title)
|
||||
.setMessage(report)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.copy_to_clipboard, null)
|
||||
.create()
|
||||
.apply {
|
||||
setOnShowListener {
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||
clipboard.primaryClip = ClipData.newPlainText(
|
||||
context!!.getString(R.string.settings_last_error_title),
|
||||
report
|
||||
)
|
||||
|
||||
Toast.makeText(context, context!!.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.BottomSheetDialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.databinding.DialogFileBinding
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileSpec
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
|
||||
class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val ARG_FILE_SPEC = "file spec"
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
private const val REQ_SAVE_AS = 1
|
||||
|
||||
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
|
||||
folder = fileInfo.folder,
|
||||
path = fileInfo.path,
|
||||
fileName = fileInfo.fileName
|
||||
))
|
||||
|
||||
fun newInstance(fileSpec: DownloadFileSpec) = FileMenuDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_FILE_SPEC, fileSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val fileSpec: DownloadFileSpec by lazy {
|
||||
arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = DialogFileBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.filename = fileSpec.fileName
|
||||
|
||||
binding.saveAsButton.setOnClickListener {
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
type = MimeType.getFromFilename(fileSpec.fileName)
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
|
||||
},
|
||||
REQ_SAVE_AS
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQ_SAVE_AS -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
DownloadFileDialogFragment.newInstance(fileSpec, data!!.data!!).show(fragmentManager)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager?) {
|
||||
show(fragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
+13
-20
@@ -1,26 +1,25 @@
|
||||
package net.syncthing.lite.utils
|
||||
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,
|
||||
syncthingSubFolder: String,
|
||||
private val syncthingSubFolder: String,
|
||||
private val onUploadCompleteListener: () -> Unit) {
|
||||
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var uploadFileTask: UploadFileTask? = null
|
||||
|
||||
init {
|
||||
fun show() {
|
||||
showDialog()
|
||||
doAsync {
|
||||
uploadFileTask = UploadFileTask(context, syncthingClient, localFile, syncthingFolder,
|
||||
@@ -40,25 +39,19 @@ class FileUploadDialog(private val context: Context, private val syncthingClient
|
||||
}
|
||||
|
||||
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
|
||||
async(UI) {
|
||||
progressDialog.isIndeterminate = false
|
||||
progressDialog.progress = observer.progressPercentage()
|
||||
progressDialog.max = 100
|
||||
}
|
||||
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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
progressDialog.dismiss()
|
||||
this@FileUploadDialog.context.toast(R.string.toast_file_upload_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.AppCompatCheckBox
|
||||
import android.view.LayoutInflater
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogFolderInfoBinding
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
|
||||
class FolderInfoDialog: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
fun newInstance(folderId: String) = FolderInfoDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(FOLDER_ID, folderId)
|
||||
}
|
||||
}
|
||||
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val TAG = "FolderInfoDialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val folderId = arguments!!.getString(FOLDER_ID)
|
||||
val binding = DialogFolderInfoBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!)
|
||||
.setTitle(folderId)
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
launch {
|
||||
val configuration = libraryHandler.libraryManager.withLibrary { it.configuration }
|
||||
|
||||
val folderInfo = configuration.folders.find { it.folderId == folderId }
|
||||
|
||||
if (folderInfo == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return@launch
|
||||
}
|
||||
|
||||
dialog.setTitle(folderInfo.label)
|
||||
|
||||
binding.deviceCheckboxesContainer.removeAllViews()
|
||||
|
||||
val allRelatedDevices = (folderInfo.deviceIdWhitelist + folderInfo.deviceIdBlacklist).toSet()
|
||||
|
||||
allRelatedDevices.forEach { deviceId ->
|
||||
val deviceInfo = configuration.peers.find { it.deviceId == deviceId }
|
||||
|
||||
val deviceLabel = if (deviceInfo == null)
|
||||
deviceId.deviceId
|
||||
else
|
||||
context!!.getString(R.string.dialog_folder_info_device_list_item, deviceInfo.name, deviceId.deviceId)
|
||||
|
||||
binding.deviceCheckboxesContainer.addView(
|
||||
AppCompatCheckBox(context!!).apply {
|
||||
text = deviceLabel
|
||||
isChecked = folderInfo.deviceIdWhitelist.contains(deviceId)
|
||||
|
||||
setOnCheckedChangeListener { _, isShared ->
|
||||
this@FolderInfoDialog.launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
// update the config
|
||||
library.configuration.update { oldConfig ->
|
||||
val oldFolders = oldConfig.folders
|
||||
var folderToChange = oldFolders.find { it.folderId == folderId }!!
|
||||
val foldersNotToChange = oldFolders.filterNot { it.folderId == folderId }.toSet()
|
||||
|
||||
if (isShared) {
|
||||
folderToChange = folderToChange.copy(
|
||||
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdBlacklist = folderToChange.deviceIdBlacklist.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdWhitelist = folderToChange.deviceIdWhitelist + setOf(deviceId)
|
||||
)
|
||||
} else {
|
||||
folderToChange = folderToChange.copy(
|
||||
deviceIdWhitelist = folderToChange.deviceIdWhitelist.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdBlacklist = folderToChange.deviceIdBlacklist + setOf(deviceId),
|
||||
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList + setOf(deviceId)
|
||||
)
|
||||
}
|
||||
|
||||
oldConfig.copy(folders = foldersNotToChange + folderToChange)
|
||||
}
|
||||
library.configuration.persistLater()
|
||||
|
||||
// apply the change
|
||||
library.syncthingClient.reconnect(deviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentActivity
|
||||
import android.support.v7.app.AlertDialog
|
||||
import net.syncthing.lite.R
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
class ReconnectIssueDialogFragment: DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?) = AlertDialog.Builder(context!!, theme)
|
||||
.setMessage(R.string.dialog_warning_reconnect_problem)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
context!!.defaultSharedPreferences.edit()
|
||||
.putBoolean(SETTINGS_PARAM, true)
|
||||
.apply()
|
||||
}
|
||||
.create()
|
||||
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "ReconnectIssueDialog"
|
||||
private const val SETTINGS_PARAM = "has_educated_about_reconnect_issues"
|
||||
|
||||
fun showIfNeeded(activity: FragmentActivity) {
|
||||
if (!activity.defaultSharedPreferences.getBoolean(SETTINGS_PARAM, false)) {
|
||||
if (activity.supportFragmentManager.findFragmentByTag(DIALOG_TAG) == null) {
|
||||
ReconnectIssueDialogFragment().show(activity.supportFragmentManager, DIALOG_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package net.syncthing.lite.dialogs.downloadfile
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.util.Log
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.CacheFileProviderUrl
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
import org.jetbrains.anko.newTask
|
||||
import org.jetbrains.anko.toast
|
||||
|
||||
class DownloadFileDialogFragment : DialogFragment() {
|
||||
companion object {
|
||||
private const val ARG_FILE_SPEC = "file spec"
|
||||
private const val ARG_SAVE_AS_URI = "save as"
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
|
||||
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
|
||||
folder = fileInfo.folder,
|
||||
path = fileInfo.path,
|
||||
fileName = fileInfo.fileName
|
||||
))
|
||||
|
||||
fun newInstance(
|
||||
fileSpec: DownloadFileSpec,
|
||||
outputUri: Uri? = null
|
||||
) = DownloadFileDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_FILE_SPEC, fileSpec)
|
||||
|
||||
if (outputUri != null) {
|
||||
putParcelable(ARG_SAVE_AS_URI, outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val model: DownloadFileDialogViewModel by lazy {
|
||||
ViewModelProviders.of(this).get(DownloadFileDialogViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val fileSpec = arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
|
||||
val outputUri = if (arguments!!.containsKey(ARG_SAVE_AS_URI))
|
||||
arguments!!.getParcelable(ARG_SAVE_AS_URI) as Uri
|
||||
else
|
||||
null
|
||||
|
||||
model.init(
|
||||
libraryHandler = LibraryHandler(context!!),
|
||||
fileSpec = fileSpec,
|
||||
externalCacheDir = context!!.externalCacheDir,
|
||||
outputUri = outputUri,
|
||||
contentResolver = context!!.contentResolver
|
||||
)
|
||||
|
||||
val progressDialog = ProgressDialog(context).apply {
|
||||
setMessage(context!!.getString(R.string.dialog_downloading_file, fileSpec.fileName))
|
||||
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
isCancelable = true
|
||||
isIndeterminate = true
|
||||
max = DownloadFileStatusRunning.MAX_PROGRESS
|
||||
}
|
||||
|
||||
model.status.observe(this, Observer {
|
||||
status ->
|
||||
|
||||
when (status) {
|
||||
is DownloadFileStatusRunning -> {
|
||||
progressDialog.apply {
|
||||
isIndeterminate = false
|
||||
progress = status.progress
|
||||
}
|
||||
}
|
||||
is DownloadFileStatusDone -> {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
if (outputUri == null) {
|
||||
val mimeType = MimeType.getFromFilename(fileSpec.fileName)
|
||||
|
||||
try {
|
||||
context!!.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(
|
||||
CacheFileProviderUrl.fromFile(
|
||||
filename = fileSpec.fileName,
|
||||
mimeType = mimeType,
|
||||
file = status.file,
|
||||
context = context!!
|
||||
).serialized,
|
||||
mimeType
|
||||
)
|
||||
.newTask()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "No handler found for file " + status.file.name, e)
|
||||
}
|
||||
|
||||
context!!.toast(R.string.toast_open_file_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
is DownloadFileStatusFailed -> {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
context!!.toast(R.string.toast_file_download_failed)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return progressDialog
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface?) {
|
||||
super.onCancel(dialog)
|
||||
|
||||
model.cancel()
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager?) {
|
||||
show(fragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package net.syncthing.lite.dialogs.downloadfile
|
||||
|
||||
import android.arch.lifecycle.LiveData
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.support.v4.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.library.DownloadFileTask
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
|
||||
class DownloadFileDialogViewModel : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
}
|
||||
|
||||
private var isInitialized = false
|
||||
private val statusInternal = MutableLiveData<DownloadFileStatus>()
|
||||
private val cancellationSignal = CancellationSignal()
|
||||
val status: LiveData<DownloadFileStatus> = statusInternal
|
||||
|
||||
fun init(
|
||||
libraryHandler: LibraryHandler,
|
||||
fileSpec: DownloadFileSpec,
|
||||
externalCacheDir: File,
|
||||
outputUri: Uri?,
|
||||
contentResolver: ContentResolver
|
||||
) {
|
||||
if (isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
|
||||
libraryHandler.start()
|
||||
|
||||
// this keeps the client only active as long as the block is running
|
||||
// but the file downloading is not synchronous.
|
||||
// Due to that, the start and stop calls are used.
|
||||
libraryHandler.syncthingClient {
|
||||
syncthingClient ->
|
||||
|
||||
try {
|
||||
val fileInfo = syncthingClient.indexHandler.getFileInfoByPath(
|
||||
folder = fileSpec.folder,
|
||||
path = fileSpec.path
|
||||
)!!
|
||||
|
||||
val task = DownloadFileTask(
|
||||
fileStorageDirectory = externalCacheDir,
|
||||
syncthingClient = syncthingClient,
|
||||
fileInfo = fileInfo,
|
||||
onProgress = { status ->
|
||||
val newProgress = (status.downloadedBytes * DownloadFileStatusRunning.MAX_PROGRESS / status.totalTransferSize).toInt()
|
||||
val currentStatus = statusInternal.value
|
||||
|
||||
// only update if it changed
|
||||
if (!(currentStatus is DownloadFileStatusRunning) || currentStatus.progress != newProgress) {
|
||||
statusInternal.value = DownloadFileStatusRunning(newProgress)
|
||||
}
|
||||
},
|
||||
onComplete = { file ->
|
||||
libraryHandler.stop()
|
||||
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
if (outputUri != null) {
|
||||
contentResolver.openOutputStream(outputUri).use { outputStream ->
|
||||
FileUtils.copyFile(file, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusDone(file))
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "downloading file failed", ex)
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusFailed)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
statusInternal.value = DownloadFileStatusFailed
|
||||
|
||||
libraryHandler.stop()
|
||||
}
|
||||
)
|
||||
|
||||
cancellationSignal.setOnCancelListener {
|
||||
task.cancel()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "downloading file failed", ex)
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
cancellationSignal.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package net.syncthing.lite.dialogs.downloadfile
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class DownloadFileSpec(val folder: String, val path: String, val fileName: String): Serializable
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.syncthing.lite.dialogs.downloadfile
|
||||
|
||||
import java.io.File
|
||||
|
||||
sealed class DownloadFileStatus
|
||||
object DownloadFileStatusFailed: DownloadFileStatus()
|
||||
data class DownloadFileStatusDone(val file: File): DownloadFileStatus()
|
||||
data class DownloadFileStatusRunning(val progress: Int): DownloadFileStatus() {
|
||||
companion object {
|
||||
const val MAX_PROGRESS = 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.syncthing.lite.error
|
||||
|
||||
import android.content.Context
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
object ErrorStorage {
|
||||
private const val PREF_KEY = "last_error"
|
||||
|
||||
fun reportError(context: Context, error: String) {
|
||||
// this uses commit because the App could be quit directly after that
|
||||
context.defaultSharedPreferences.edit()
|
||||
.putString(PREF_KEY, error)
|
||||
.commit()
|
||||
}
|
||||
|
||||
fun getLastErrorReport(context: Context) = context.defaultSharedPreferences.getString(PREF_KEY, "there is no saved report")
|
||||
}
|
||||
@@ -9,152 +9,116 @@ 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.DeviceId
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.DeviceAdapterListener
|
||||
import net.syncthing.lite.adapters.DevicesAdapter
|
||||
import net.syncthing.lite.databinding.FragmentDevicesBinding
|
||||
import net.syncthing.lite.databinding.ViewEnterDeviceIdBinding
|
||||
import net.syncthing.lite.utils.FragmentIntentIntegrator
|
||||
import org.apache.commons.lang3.StringUtils.isBlank
|
||||
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
|
||||
import uk.co.markormesher.android_fab.SpeedDialMenuItem
|
||||
import net.syncthing.lite.utils.Util
|
||||
import java.io.IOException
|
||||
import java.security.InvalidParameterException
|
||||
|
||||
class DevicesFragment : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentDevicesBinding
|
||||
private lateinit var adapter: DevicesAdapter
|
||||
private val 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.fab.speedDialMenuAdapter = FabMenuAdapter()
|
||||
binding.addDevice.setOnClickListener { showDialog() }
|
||||
|
||||
binding.list.adapter = adapter
|
||||
|
||||
adapter.listener = object: DeviceAdapterListener {
|
||||
override fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(getString(R.string.remove_device_title, deviceInfo.name))
|
||||
.setMessage(getString(R.string.remove_device_message, deviceInfo.deviceId.deviceId.substring(0, 7)))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
library.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers
|
||||
.filterNot { it.deviceId == deviceInfo.deviceId }
|
||||
.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
library.configuration.persistLater()
|
||||
|
||||
// TODO: update the device list (should become a side effect of the call below)
|
||||
library.syncthingClient.disconnectFromRemovedDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
libraryHandler.subscribeToConnectionStatus().consumeEach { connectionInfo ->
|
||||
val devices = libraryHandler.libraryManager.withLibrary { it.configuration.peers }
|
||||
|
||||
adapter.data = devices.map { device -> device to (connectionInfo[device.deviceId] ?: ConnectionInfo.empty) }
|
||||
binding.isEmpty = devices.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener(this::onConnectionChanged) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener(this::onConnectionChanged) }
|
||||
}
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
updateDeviceList()
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
initDeviceList()
|
||||
updateDeviceList()
|
||||
}
|
||||
|
||||
private fun initDeviceList() {
|
||||
adapter = DevicesAdapter(context!!)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemLongClickListener { _, _, position, _ ->
|
||||
val device = adapter.getItem(position)
|
||||
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)))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
libraryHandler?.configuration { config ->
|
||||
config.peers = config.peers.filterNot { it.deviceId == device.deviceId }.toSet()
|
||||
config.persistLater()
|
||||
updateDeviceList()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDeviceList() {
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
adapter.clear()
|
||||
adapter.addAll(syncthingClient.getPeerStatus())
|
||||
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 != null) {
|
||||
val deviceId = scanResult.contents
|
||||
if (!isBlank(deviceId)) {
|
||||
importDeviceId(deviceId)
|
||||
}
|
||||
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
|
||||
addDeviceDialogBinding?.deviceId?.setText(scanResult.contents)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importDeviceId(deviceIdString: String) {
|
||||
libraryHandler?.configuration { configuration ->
|
||||
async(UI) {
|
||||
val deviceId =
|
||||
try {
|
||||
DeviceId(deviceIdString)
|
||||
} catch (e: IOException) {
|
||||
Toast.makeText(this@DevicesFragment.context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
|
||||
return@async
|
||||
}
|
||||
private fun showDialog() {
|
||||
val binding = ViewEnterDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
addDeviceDialogBinding = binding
|
||||
|
||||
if (!configuration.peerIds.contains(deviceId)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId, null)
|
||||
configuration.persistLater()
|
||||
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_import_success, deviceId), Toast.LENGTH_SHORT).show()
|
||||
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
|
||||
} else {
|
||||
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_already_known, deviceId), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
val dialog = 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()
|
||||
|
||||
addDeviceDialog = dialog
|
||||
|
||||
fun handleAddClick() {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { /* TODO: Is updateDeviceList() still required? */ })
|
||||
dialog.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
|
||||
}
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)!!.setOnClickListener { handleAddClick() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,54 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
import net.syncthing.lite.adapters.FolderListAdapterListener
|
||||
import net.syncthing.lite.adapters.FoldersListAdapter
|
||||
import net.syncthing.lite.databinding.FragmentFoldersBinding
|
||||
import net.syncthing.lite.dialogs.FolderInfoDialog
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class FoldersFragment : SyncthingFragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val adapter = FoldersListAdapter()
|
||||
|
||||
private val TAG = "FoldersFragment"
|
||||
adapter.listener = object : FolderListAdapterListener {
|
||||
override fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats) {
|
||||
startActivity(
|
||||
activity!!.intentFor<FolderBrowserActivity>(
|
||||
FolderBrowserActivity.EXTRA_FOLDER_NAME to folderInfo.folderId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentFoldersBinding
|
||||
override fun onFolderLongClicked(folderInfo: FolderInfo): Boolean {
|
||||
FolderInfoDialog
|
||||
.newInstance(folderId = folderInfo.folderId)
|
||||
.show(fragmentManager!!)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_folders, container, false)
|
||||
binding.list.emptyView = binding.empty
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
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)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
super.onIndexUpdateComplete(folderInfo)
|
||||
showAllFoldersListView()
|
||||
val binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
|
||||
binding.list.adapter = adapter
|
||||
libraryHandler.isListeningPortTaken.observe(this, Observer { binding.listeningPortTaken = it })
|
||||
|
||||
launch {
|
||||
libraryHandler.subscribeToFolderStatusList().consumeEach {
|
||||
adapter.data = it
|
||||
binding.isEmpty = it.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.dialogs.ErrorReportDialog
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
import net.syncthing.lite.library.DefaultLibraryManager
|
||||
|
||||
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")
|
||||
val forceStop = findPreference("force_stop")
|
||||
val lastCrash = findPreference("last_crash")
|
||||
val reportBug = findPreference("report_bug")
|
||||
val libraryManager = DefaultLibraryManager.with(context!!)
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryManager.withLibrary { library ->
|
||||
localDeviceName.text = library.configuration.localDeviceName
|
||||
}
|
||||
}
|
||||
|
||||
appVersion.summary = context!!.packageManager.getPackageInfo(context!!.packageName, 0)?.versionName
|
||||
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
val newDeviceName = localDeviceName.text
|
||||
|
||||
GlobalScope.launch {
|
||||
libraryManager.withLibrary { library ->
|
||||
library.configuration.update { it.copy(localDeviceName = newDeviceName) }
|
||||
library.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
forceStop.setOnPreferenceClickListener {
|
||||
System.exit(0)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
lastCrash.setOnPreferenceClickListener {
|
||||
ErrorReportDialog.newInstance(ErrorStorage.getLastErrorReport(context!!)).show(fragmentManager!!)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
reportBug.setOnPreferenceClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/syncthing/syncthing-lite/issues")))
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import net.syncthing.lite.async.CoroutineDialogFragment
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
|
||||
abstract class SyncthingDialogFragment : CoroutineDialogFragment() {
|
||||
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
|
||||
context = context!!
|
||||
)}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
libraryHandler.start()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
libraryHandler.stop()
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,25 @@
|
||||
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.async.CoroutineFragment
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
|
||||
abstract class SyncthingFragment : Fragment() {
|
||||
abstract class SyncthingFragment : CoroutineFragment() {
|
||||
val libraryHandler: LibraryHandler by lazy { LibraryHandler(context = context!!)}
|
||||
|
||||
var libraryHandler: LibraryHandler? = null
|
||||
private set
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
|
||||
this::onIndexUpdateComplete)
|
||||
libraryHandler.start {
|
||||
// TODO: check if this is still useful
|
||||
onLibraryLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
onLibraryLoaded()
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
libraryHandler?.close()
|
||||
libraryHandler.stop()
|
||||
}
|
||||
|
||||
open fun onLibraryLoaded() {}
|
||||
|
||||
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
|
||||
|
||||
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class CacheFileProvider: ContentProvider() {
|
||||
companion object {
|
||||
const val AUTHORITY = "net.syncthing.lite.fileprovider"
|
||||
}
|
||||
|
||||
override fun onCreate() = true
|
||||
|
||||
override fun insert(uri: Uri?, values: ContentValues?): Uri {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor {
|
||||
val url = CacheFileProviderUrl.fromUri(uri)
|
||||
val file = url.getFile(context)
|
||||
|
||||
val resultProjection = projection ?: arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
||||
val resultCursor = MatrixCursor(resultProjection)
|
||||
|
||||
if (file.exists()) {
|
||||
val builder = resultCursor.newRow()
|
||||
|
||||
for (row in resultProjection) {
|
||||
when (row) {
|
||||
OpenableColumns.DISPLAY_NAME -> builder.add(url.filename)
|
||||
OpenableColumns.SIZE -> builder.add(file.length())
|
||||
else -> builder.add(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultCursor
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String = CacheFileProviderUrl.fromUri(uri).mimeType
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||
if (mode == "r") {
|
||||
val url = CacheFileProviderUrl.fromUri(uri)
|
||||
val file = url.getFile(context)
|
||||
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} else {
|
||||
throw IOException("illegal mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CacheFileProviderUrl(
|
||||
val pathInCacheDirectory: String,
|
||||
val filename: String,
|
||||
val mimeType: String
|
||||
) {
|
||||
companion object {
|
||||
private const val PATH = "path"
|
||||
private const val FILENAME = "filename"
|
||||
private const val MIME_TYPE = "mimeType"
|
||||
|
||||
fun fromUri(uri: Uri) = CacheFileProviderUrl(
|
||||
pathInCacheDirectory = uri.getQueryParameter(PATH),
|
||||
filename = uri.getQueryParameter(FILENAME),
|
||||
mimeType = uri.getQueryParameter(MIME_TYPE)
|
||||
)
|
||||
|
||||
fun fromFile(file: File, filename: String, mimeType: String, context: Context) = CacheFileProviderUrl(
|
||||
filename = filename,
|
||||
mimeType = mimeType,
|
||||
pathInCacheDirectory = file.toRelativeString(context.externalCacheDir)
|
||||
)
|
||||
}
|
||||
|
||||
val serialized: Uri by lazy {
|
||||
Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(CacheFileProvider.AUTHORITY)
|
||||
.appendQueryParameter(PATH, pathInCacheDirectory)
|
||||
.appendQueryParameter(FILENAME, filename)
|
||||
.appendQueryParameter(MIME_TYPE, mimeType)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getFile(context: Context): File {
|
||||
return File(context.externalCacheDir, pathInCacheDirectory)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
object DefaultLibraryManager {
|
||||
private const val LOG_TAG = "DefaultLibraryManager"
|
||||
|
||||
private var instance: LibraryManager? = null
|
||||
private val lock = Object()
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun with(context: Context) = withApplicationContext(context.applicationContext)
|
||||
|
||||
private fun withApplicationContext(context: Context): LibraryManager {
|
||||
if (instance == null) {
|
||||
synchronized(lock) {
|
||||
if (instance == null) {
|
||||
val shutdownRunnable = Runnable {
|
||||
instance!!.shutdownIfThereAreZeroUsers()
|
||||
}
|
||||
|
||||
fun scheduleShutdown() {
|
||||
val shutdownDelay = context.defaultSharedPreferences.getString(
|
||||
"shutdown_delay",
|
||||
context.getString(R.string.default_shutdown_delay)
|
||||
).toLong()
|
||||
|
||||
handler.postDelayed(shutdownRunnable, shutdownDelay)
|
||||
}
|
||||
|
||||
fun cancelShutdown() {
|
||||
handler.removeCallbacks(shutdownRunnable)
|
||||
}
|
||||
|
||||
instance = LibraryManager(
|
||||
synchronousInstanceCreator = {
|
||||
LibraryInstance(context) { ex ->
|
||||
// this delay ensures that the toast is shown even if the UI thread is busy
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, R.string.toast_error, Toast.LENGTH_LONG).show()
|
||||
}, 100L)
|
||||
|
||||
ErrorStorage.reportError(context, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
|
||||
}
|
||||
},
|
||||
userCounterListener = {
|
||||
newUserCounter ->
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "user counter updated to $newUserCounter")
|
||||
}
|
||||
|
||||
val isUsed = newUserCounter > 0
|
||||
|
||||
if (isUsed) {
|
||||
cancelShutdown()
|
||||
} else {
|
||||
scheduleShutdown()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import java.io.File
|
||||
|
||||
data class DownloadFilePath (val baseDirectory: File, val fileHash: String) {
|
||||
val filesDirectory = File(baseDirectory, fileHash.substring(0, 2))
|
||||
val targetFile = File(filesDirectory, fileHash.substring(2))
|
||||
val tempFile = File(filesDirectory, fileHash.substring(2) + "_temp")
|
||||
}
|
||||
@@ -1,49 +1,143 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.syncthing.java.bep.BlockPullerStatus
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
class DownloadFileTask(private val context: Context, syncthingClient: SyncthingClient,
|
||||
class DownloadFileTask(private val fileStorageDirectory: File,
|
||||
syncthingClient: SyncthingClient,
|
||||
private val fileInfo: FileInfo,
|
||||
private val onProgress: (DownloadFileTask, BlockPuller.FileDownloadObserver) -> Unit,
|
||||
private val onProgress: (status: BlockPullerStatus) -> Unit,
|
||||
private val onComplete: (File) -> Unit,
|
||||
private val onError: () -> Unit) {
|
||||
private val onError: (Exception) -> Unit) {
|
||||
|
||||
private val Tag = "DownloadFileTask"
|
||||
private var isCancelled = false
|
||||
companion object {
|
||||
private const val TAG = "DownloadFileTask"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
suspend fun downloadFileCoroutine(
|
||||
externalCacheDir: File,
|
||||
syncthingClient: SyncthingClient,
|
||||
fileInfo: FileInfo,
|
||||
onProgress: (status: BlockPullerStatus) -> Unit
|
||||
) = suspendCancellableCoroutine<File> {
|
||||
continuation ->
|
||||
|
||||
val task = DownloadFileTask(
|
||||
externalCacheDir,
|
||||
syncthingClient,
|
||||
fileInfo,
|
||||
onProgress,
|
||||
{
|
||||
continuation.resume(it)
|
||||
},
|
||||
{
|
||||
continuation.resumeWithException(it)
|
||||
}
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cancellationSignal = CancellationSignal()
|
||||
private var doneListenerCalled = false
|
||||
|
||||
init {
|
||||
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
|
||||
val observer = blockPuller.pullFile(fileInfo)
|
||||
onProgress(this, observer)
|
||||
try {
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPuller
|
||||
val file = DownloadFilePath(fileStorageDirectory, fileInfo.hash!!)
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i("pullFile", "download progress = " + observer.progressMessage())
|
||||
onProgress(this, observer)
|
||||
GlobalScope.launch {
|
||||
if (file.targetFile.exists()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "there is already a file")
|
||||
}
|
||||
|
||||
val outputFile = File("${context.externalCacheDir}/${fileInfo.folder}/${fileInfo.path}")
|
||||
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
|
||||
Log.i(Tag, "Downloaded file $fileInfo")
|
||||
onComplete(outputFile)
|
||||
} catch (e: IOException) {
|
||||
onError()
|
||||
Log.w(Tag, "Failed to download file $fileInfo", e)
|
||||
callComplete(file.targetFile)
|
||||
|
||||
return@launch
|
||||
}
|
||||
}, { onError() })
|
||||
|
||||
try {
|
||||
if (!file.filesDirectory.isDirectory) {
|
||||
if (!file.filesDirectory.mkdirs()) {
|
||||
throw IOException("could not create output directory")
|
||||
}
|
||||
}
|
||||
|
||||
// download the file to a temp location
|
||||
val inputStream = syncthingClient.pullFile(fileInfo, this@DownloadFileTask::callProgress)
|
||||
|
||||
try {
|
||||
FileUtils.copyInputStreamToFile(inputStream, file.tempFile)
|
||||
file.tempFile.renameTo(file.targetFile)
|
||||
} finally {
|
||||
file.tempFile.delete()
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(TAG, "Downloaded file $fileInfo")
|
||||
}
|
||||
|
||||
callComplete(file.targetFile)
|
||||
} catch (e: Exception) {
|
||||
callError(e)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "Failed to download file $fileInfo", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun callProgress(status: BlockPullerStatus) {
|
||||
handler.post {
|
||||
if (!doneListenerCalled) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i("pullFile", "download progress = $status")
|
||||
}
|
||||
|
||||
onProgress(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun callComplete(file: File) {
|
||||
handler.post {
|
||||
if (!doneListenerCalled) {
|
||||
doneListenerCalled = true
|
||||
|
||||
onComplete(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun callError(exception: Exception) {
|
||||
handler.post {
|
||||
if (!doneListenerCalled) {
|
||||
doneListenerCalled = true
|
||||
|
||||
onError(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
isCancelled = true
|
||||
cancellationSignal.cancel()
|
||||
callError(InterruptedException())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,125 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.arch.lifecycle.LiveData
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import android.os.Looper
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.bep.folder.FolderBrowser
|
||||
import net.syncthing.java.bep.folder.FolderStatus
|
||||
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.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
|
||||
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
|
||||
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit) {
|
||||
/**
|
||||
* This class helps when using the library.
|
||||
* It's required to start and stop it to make the callbacks fire (or stop to fire).
|
||||
*
|
||||
* It's possible to do multiple start and stop cycles with one instance of this class.
|
||||
*/
|
||||
class LibraryHandler(context: Context) {
|
||||
|
||||
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
|
||||
private const val TAG = "LibraryHandler"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
private val TAG = "LibraryHandler"
|
||||
val libraryManager = DefaultLibraryManager.with(context)
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { postValue(false) }
|
||||
private val indexUpdateCompleteMessages = BroadcastChannel<String>(capacity = 16)
|
||||
private val folderStatusList = BroadcastChannel<List<FolderStatus>>(capacity = Channel.CONFLATED)
|
||||
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>()
|
||||
private var job: Job = Job()
|
||||
|
||||
init {
|
||||
instanceCount++
|
||||
if (configuration == null && !isLoading) {
|
||||
isLoading = true
|
||||
doAsync {
|
||||
init(context)
|
||||
async(UI) {
|
||||
onLibraryLoaded(this@LibraryHandler)
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
onLibraryLoaded(this)
|
||||
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
|
||||
|
||||
private val messageFromUnknownDeviceListeners = HashSet<(DeviceId) -> Unit>()
|
||||
private val internalMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {
|
||||
deviceId ->
|
||||
|
||||
handler.post {
|
||||
messageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun start(onLibraryLoaded: (LibraryHandler) -> Unit = {}) {
|
||||
if (isStarted.getAndSet(true) == true) {
|
||||
throw IllegalStateException("already started")
|
||||
}
|
||||
|
||||
libraryManager.startLibraryUsage {
|
||||
libraryInstance ->
|
||||
|
||||
isListeningPortTakenInternal.value = libraryInstance.isListeningPortTaken
|
||||
onLibraryLoaded(this)
|
||||
|
||||
val client = libraryInstance.syncthingClient
|
||||
|
||||
client.discoveryHandler.registerMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
|
||||
|
||||
job = Job()
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
libraryInstance.syncthingClient.indexHandler.subscribeToOnFullIndexAcquiredEvents().consumeEach {
|
||||
indexUpdateCompleteMessages.send(it)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
libraryInstance.folderBrowser.folderInfoAndStatusStream().consumeEach {
|
||||
folderStatusList.send(it)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
libraryInstance.syncthingClient.subscribeToConnectionStatus().consumeEach {
|
||||
connectionStatus.send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (isStarted.getAndSet(false) == false) {
|
||||
throw IllegalStateException("already stopped")
|
||||
}
|
||||
|
||||
job!!.cancel()
|
||||
|
||||
syncthingClient {
|
||||
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
|
||||
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
|
||||
try {
|
||||
it.discoveryHandler.unregisterMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignored, no idea why this is thrown
|
||||
}
|
||||
}
|
||||
|
||||
libraryManager.stopLibraryUsage()
|
||||
}
|
||||
|
||||
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, cacheFolder = context.externalCacheDir)
|
||||
configuration.localDeviceName = Util.getDeviceName()
|
||||
configuration.persistLater()
|
||||
val syncthingClient = SyncthingClient(configuration)
|
||||
//TODO listen for device events, update device list
|
||||
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
|
||||
}
|
||||
|
||||
private 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)
|
||||
/*
|
||||
* The callback is executed asynchronously.
|
||||
* As soon as it returns, there is no guarantee about the availability of the library
|
||||
*/
|
||||
fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
|
||||
libraryManager.startLibraryUsage {
|
||||
doAsync {
|
||||
try {
|
||||
callback(it.configuration, it.syncthingClient, it.folderBrowser)
|
||||
} finally {
|
||||
libraryManager.stopLibraryUsage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,34 +136,17 @@ class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit
|
||||
library { _, _, f -> callback(f) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
instanceCount--
|
||||
Handler().postDelayed({
|
||||
Thread {
|
||||
if (instanceCount == 0) {
|
||||
folderBrowser?.close()
|
||||
folderBrowser = null
|
||||
syncthingClient?.close()
|
||||
syncthingClient = null
|
||||
configuration = null
|
||||
}
|
||||
}.start()
|
||||
}, 60 * 1000)
|
||||
|
||||
// these listeners are called at the UI Thread
|
||||
// there is no need to unregister because they removed from the library when close is called
|
||||
fun registerMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) {
|
||||
messageFromUnknownDeviceListeners.add(listener)
|
||||
}
|
||||
|
||||
fun unregisterMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) {
|
||||
messageFromUnknownDeviceListeners.remove(listener)
|
||||
}
|
||||
|
||||
fun subscribeToOnFullIndexAcquiredEvents() = indexUpdateCompleteMessages.openSubscription()
|
||||
fun subscribeToFolderStatusList() = folderStatusList.openSubscription()
|
||||
fun subscribeToConnectionStatus() = connectionStatus.openSubscription()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.repository.EncryptedTempRepository
|
||||
import net.syncthing.repository.android.SqliteIndexRepository
|
||||
import net.syncthing.repository.android.TempDirectoryLocalRepository
|
||||
import net.syncthing.repository.android.database.RepositoryDatabase
|
||||
import java.io.File
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.SocketException
|
||||
|
||||
/**
|
||||
* This class is used internally to access the syncthing-java library
|
||||
* There should be never more than 1 instance of this class
|
||||
*
|
||||
* This class can not be recycled. This means that after doing a shutdown of it,
|
||||
* a new instance must be created
|
||||
*
|
||||
* The creation and the shutdown are synchronous, so keep them out of the UI Thread
|
||||
*/
|
||||
class LibraryInstance (
|
||||
context: Context,
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "LibraryInstance"
|
||||
|
||||
/**
|
||||
* Check if listening port for local discovery is taken by another app. Do this check here to
|
||||
* avoid adding another callback.
|
||||
*/
|
||||
private fun checkIsListeningPortTaken(): Boolean {
|
||||
try {
|
||||
DatagramSocket(21027, InetAddress.getByName("0.0.0.0")).close()
|
||||
|
||||
return false
|
||||
} catch (e: SocketException) {
|
||||
Log.w(LOG_TAG, e)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val tempRepository = EncryptedTempRepository(
|
||||
TempDirectoryLocalRepository(
|
||||
File(context.filesDir, "temp_repository")
|
||||
)
|
||||
)
|
||||
|
||||
val isListeningPortTaken = checkIsListeningPortTaken() // this must come first to work correctly
|
||||
val configuration = Configuration(configFolder = context.filesDir)
|
||||
val syncthingClient = SyncthingClient(
|
||||
configuration = configuration,
|
||||
repository = SqliteIndexRepository(
|
||||
database = RepositoryDatabase.with(context),
|
||||
closeDatabaseOnClose = false,
|
||||
clearTempStorageHook = { tempRepository.deleteAllTempData() }
|
||||
),
|
||||
tempRepository = tempRepository,
|
||||
exceptionReportHandler = { ex ->
|
||||
Log.w(LOG_TAG, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
exceptionReportHandler(ex)
|
||||
}
|
||||
}
|
||||
)
|
||||
val folderBrowser = syncthingClient.indexHandler.folderBrowser
|
||||
val indexBrowser = syncthingClient.indexHandler.indexBrowser
|
||||
|
||||
suspend fun shutdown() {
|
||||
syncthingClient.close()
|
||||
configuration.persistNow()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* This class manages the access to an LibraryInstance
|
||||
*
|
||||
* Users can get an instance with startLibraryUsage()
|
||||
* If they are done with it, the should call stopLibraryUsage()
|
||||
* After this, it's NOT safe to continue using the received LibraryInstance
|
||||
*
|
||||
* Every call to startLibraryUsage should be followed by an call to stopLibraryUsage,
|
||||
* even if the callback was not called yet. It can still be called, so users should watch out.
|
||||
*
|
||||
* All listeners are executed at the UI Thread (except the synchronousInstanceCreator)
|
||||
*
|
||||
* The userCounterListener is always called before the isRunningListener
|
||||
*
|
||||
* The listeners are called for all changes, nothing is skipped or batched
|
||||
*/
|
||||
class LibraryManager (
|
||||
val synchronousInstanceCreator: () -> LibraryInstance,
|
||||
val userCounterListener: (Int) -> Unit = {},
|
||||
val isRunningListener: (isRunning: Boolean) -> Unit = {}
|
||||
) {
|
||||
companion object {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
// this must be a SingleThreadExecutor to avoid race conditions
|
||||
// only this Thread should access instance and userCounter
|
||||
private val startStopExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
private val instanceStream = ConflatedBroadcastChannel<LibraryInstance?>(null)
|
||||
private var userCounter = 0
|
||||
|
||||
fun startLibraryUsage(callback: (LibraryInstance) -> Unit) {
|
||||
startStopExecutor.submit {
|
||||
val newUserCounter = ++userCounter
|
||||
handler.post { userCounterListener(newUserCounter) }
|
||||
|
||||
if (instanceStream.value == null) {
|
||||
instanceStream.offer(synchronousInstanceCreator())
|
||||
handler.post { isRunningListener(true) }
|
||||
}
|
||||
|
||||
handler.post { callback(instanceStream.value!!) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startLibraryUsageCoroutine(): LibraryInstance {
|
||||
return suspendCoroutine { continuation ->
|
||||
startLibraryUsage { instance ->
|
||||
continuation.resume(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> withLibrary(action: suspend (LibraryInstance) -> T): T {
|
||||
val instance = startLibraryUsageCoroutine()
|
||||
|
||||
return try {
|
||||
action(instance)
|
||||
} finally {
|
||||
stopLibraryUsage()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopLibraryUsage() {
|
||||
startStopExecutor.submit {
|
||||
val newUserCounter = --userCounter
|
||||
|
||||
if (newUserCounter < 0) {
|
||||
userCounter = 0
|
||||
|
||||
throw IllegalStateException("can not stop library usage if there are 0 users")
|
||||
}
|
||||
|
||||
handler.post { userCounterListener(newUserCounter) }
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdownIfThereAreZeroUsers(listener: (wasShutdownPerformed: Boolean) -> Unit = {}) {
|
||||
startStopExecutor.submit {
|
||||
if (userCounter == 0) {
|
||||
runBlocking { instanceStream.value?.shutdown() }
|
||||
instanceStream.offer(null)
|
||||
|
||||
handler.post { isRunningListener(false) }
|
||||
handler.post { listener(true) }
|
||||
} else {
|
||||
handler.post { listener(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun streamDirectoryListing(folder: String, path: String) = GlobalScope.produce {
|
||||
var job = Job()
|
||||
|
||||
instanceStream.openSubscription().consumeEach { instance ->
|
||||
job.cancel()
|
||||
job = Job()
|
||||
|
||||
if (instance != null) {
|
||||
async (job) {
|
||||
instance.indexBrowser.streamDirectoryListing(folder, path).consumeEach {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,34 +8,38 @@ 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 kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.bep.index.browser.DirectoryContentListing
|
||||
import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
|
||||
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 net.syncthing.lite.utils.MimeType
|
||||
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)
|
||||
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)
|
||||
Document.COLUMN_FLAGS
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
@@ -43,91 +47,123 @@ class SyncthingProvider : DocumentsProvider() {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getLibraryHandler(): LibraryHandler {
|
||||
val latch = CountDownLatch(1)
|
||||
val libraryHandler = LibraryHandler(context, { latch.countDown() }, { _, _ -> }, {})
|
||||
latch.await()
|
||||
return libraryHandler
|
||||
}
|
||||
// this instance is not started -> it connects and disconnects on demand
|
||||
private val libraryManager: LibraryManager by lazy { DefaultLibraryManager.with(context) }
|
||||
|
||||
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 runBlocking {
|
||||
libraryManager.withLibrary { instance ->
|
||||
MatrixCursor(projection ?: DefaultRootProjection).apply {
|
||||
instance.folderBrowser.folderInfoAndStatusList().forEach { folder ->
|
||||
newRow().apply {
|
||||
add(Root.COLUMN_ROOT_ID, folder.info.folderId)
|
||||
add(Root.COLUMN_SUMMARY, folder.info.label)
|
||||
add(Root.COLUMN_FLAGS, 0)
|
||||
add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
|
||||
add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.info))
|
||||
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 runBlocking {
|
||||
libraryManager.withLibrary { instance ->
|
||||
val listing = instance.indexBrowser.getDirectoryListing(
|
||||
folder = getFolderIdForDocId(parentDocumentId),
|
||||
path = getPathForDocId(parentDocumentId)
|
||||
)
|
||||
|
||||
when (listing) {
|
||||
is DirectoryNotFoundListing -> throw FileNotFoundException()
|
||||
is DirectoryContentListing -> {
|
||||
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
|
||||
|
||||
listing.entries.forEach { entry ->
|
||||
includeFile(result, entry)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
return runBlocking {
|
||||
libraryManager.withLibrary { instance ->
|
||||
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
|
||||
folder = getFolderIdForDocId(documentId),
|
||||
path = getPathForDocId(documentId)
|
||||
) ?: throw FileNotFoundException()
|
||||
|
||||
MatrixCursor(projection ?: DefaultDocumentProjection).apply {
|
||||
includeFile(this, fileInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}, {})
|
||||
return runBlocking {
|
||||
libraryManager.withLibrary { instance ->
|
||||
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
|
||||
folder = getFolderIdForDocId(documentId),
|
||||
path = getPathForDocId(documentId)
|
||||
) ?: throw FileNotFoundException()
|
||||
|
||||
signal?.setOnCancelListener {
|
||||
this.coroutineContext.cancel()
|
||||
}
|
||||
|
||||
val outputFile = DownloadFileTask.downloadFileCoroutine(
|
||||
externalCacheDir = context.externalCacheDir,
|
||||
syncthingClient = instance.syncthingClient,
|
||||
fileInfo = fileInfo,
|
||||
onProgress = { /* ignore the progress */ }
|
||||
)
|
||||
|
||||
ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
}
|
||||
}
|
||||
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)
|
||||
result.newRow().apply {
|
||||
add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
|
||||
add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
|
||||
add(Document.COLUMN_SIZE, fileInfo.size)
|
||||
add(
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
if (fileInfo.isDirectory())
|
||||
Document.MIME_TYPE_DIR
|
||||
else
|
||||
MimeType.getFromFilename(fileInfo.fileName)
|
||||
)
|
||||
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
|
||||
add(Document.COLUMN_FLAGS, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
|
||||
@@ -137,15 +173,4 @@ class SyncthingProvider : DocumentsProvider() {
|
||||
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!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
@@ -17,7 +21,10 @@ class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
|
||||
private val onComplete: () -> Unit,
|
||||
private val onError: () -> Unit) {
|
||||
|
||||
private val TAG = "UploadFileTask"
|
||||
companion object {
|
||||
private const val TAG = "UploadFileTask"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
|
||||
private val uploadStream = context.contentResolver.openInputStream(localFile)
|
||||
@@ -26,20 +33,28 @@ class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
|
||||
|
||||
init {
|
||||
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
|
||||
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
|
||||
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
|
||||
onProgress(observer)
|
||||
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
val blockPusher = syncthingClient.getBlockPusher(folderId = syncthingFolder)
|
||||
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
|
||||
|
||||
handler.post { onProgress(observer) }
|
||||
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPusher
|
||||
return@launch
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
|
||||
onProgress(observer)
|
||||
handler.post { onProgress(observer) }
|
||||
}
|
||||
IOUtils.closeQuietly(uploadStream)
|
||||
onComplete()
|
||||
}, { onError() })
|
||||
handler.post { onComplete() }
|
||||
} catch (ex: Exception) {
|
||||
handler.post { onError() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.app.AlertDialog
|
||||
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(context: Context, syncthingClient: SyncthingClient,
|
||||
private val fileInfo: FileInfo) : AlertDialog(context) {
|
||||
|
||||
private val Tag = "FileDownloadDialog"
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var downloadFileTask: DownloadFileTask? = null
|
||||
|
||||
init {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
|
||||
object MimeType {
|
||||
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private fun getFromExtension(extension: String): String {
|
||||
val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
|
||||
return mimeType ?: DEFAULT_MIME_TYPE
|
||||
}
|
||||
|
||||
fun getFromFilename(path: String) = getFromExtension(
|
||||
PathUtils.getFileExtensionFromFilename(path).toLowerCase()
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,18 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.LibraryManager
|
||||
import org.apache.commons.lang3.StringUtils.capitalize
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.IOException
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.*
|
||||
|
||||
object Util {
|
||||
|
||||
@@ -13,11 +23,11 @@ object Util {
|
||||
val manufacturer = Build.MANUFACTURER ?: ""
|
||||
val model = Build.MODEL ?: ""
|
||||
val deviceName =
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
} else {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
} else {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
return deviceName ?: "android"
|
||||
}
|
||||
|
||||
@@ -29,4 +39,37 @@ object Util {
|
||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun importDeviceId(libraryManager: LibraryManager, context: Context, deviceId: String, onComplete: () -> Unit) {
|
||||
val newDeviceId = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryManager.withLibrary { library ->
|
||||
val didAddDevice = library.configuration.update { oldConfig ->
|
||||
if (oldConfig.peers.find { it.deviceId == newDeviceId } != null) {
|
||||
// already known
|
||||
|
||||
oldConfig
|
||||
} else {
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers + DeviceInfo(newDeviceId, newDeviceId.shortId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (didAddDevice) {
|
||||
library.configuration.persistLater()
|
||||
library.syncthingClient.connectToNewlyAddedDevices()
|
||||
|
||||
context.toast(context.getString(R.string.device_import_success, newDeviceId.shortId))
|
||||
onComplete()
|
||||
} else {
|
||||
context.toast(context.getString(R.string.device_already_known, newDeviceId.shortId))
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
googleplay@nutomic.com
|
||||
@@ -0,0 +1 @@
|
||||
https://syncthing.net
|
||||
@@ -0,0 +1 @@
|
||||
en-GB
|
||||
@@ -0,0 +1,5 @@
|
||||
This project is an Android app, that works as a client for a Syncthing share (accessing Syncthing devices in the same way a client-server file sharing app access its proprietary server).
|
||||
|
||||
This is a client-oriented implementation, designed to work online by downloading and uploading files from an active device on the network (instead of synchronizing a local copy of the entire repository). This is quite different from the way the syncthing-android works, and its useful from those devices that cannot or wish not to download the entire repository (for example, mobile devices with limited storage available, wishing to access a syncthing share).
|
||||
|
||||
Source code: https://github.com/syncthing/syncthing-lite
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1 @@
|
||||
A browser app for Syncthing-compatible shares
|
||||
@@ -0,0 +1 @@
|
||||
Syncthing Lite
|
||||
@@ -0,0 +1,2 @@
|
||||
- changed up button behavior
|
||||
- updated translations
|
||||
@@ -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="#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="#FFFFFFFF"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</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="#FF000000"
|
||||
android:pathData="M13,3h-2v10h2L13,3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.74 -1.23,-5.18 -3.17,-6.83z"/>
|
||||
</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="#FF000000"
|
||||
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="#FF000000"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</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="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>
|
||||
@@ -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="#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>
|
||||
@@ -2,56 +2,43 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="isLoading"
|
||||
type="Boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:divider="?android:listDivider"
|
||||
android:showDividers="middle">
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:visibility="@{safeUnbox(isLoading) ? View.GONE : View.VISIBLE}"
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/list_view" />
|
||||
|
||||
<ListView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:id="@+id/list_view"
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</ListView>
|
||||
<ProgressBar
|
||||
android:visibility="@{safeUnbox(isLoading) ? View.VISIBLE : View.GONE}"
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/folder_list_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/main_list_view_upload_here_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
app:elevation="6dp"
|
||||
app:pressedTranslationZ="12dp"
|
||||
android:src="@drawable/ic_file_upload_white_24dp"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/main_list_view_upload_here_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
app:elevation="6dp"
|
||||
app:pressedTranslationZ="12dp"
|
||||
android:src="@drawable/ic_file_upload_white_24dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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>
|
||||
|
||||
<ViewFlipper
|
||||
android:id="@+id/flipper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
</ViewFlipper>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="filename"
|
||||
type="String" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:padding="8dp"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{filename}"
|
||||
tools:text="Filename.type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:drawableStart="@drawable/ic_save_black_24dp"
|
||||
android:id="@+id/save_as_button"
|
||||
android:padding="8dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:text="@string/dialog_file_save_as"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/dialog_folder_info_device_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/device_checkboxes_container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<!--
|
||||
<CheckBox
|
||||
android:text="Test device 1 (the very very very very very very very very very very very long id)"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<CheckBox
|
||||
android:text="Test device 2 (the very very very very very very very very very very very long id)"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -2,32 +2,44 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="isEmpty"
|
||||
type="Boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/list"
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</ListView>
|
||||
<TextView
|
||||
android:id="@+id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="@string/devices_list_view_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<uk.co.markormesher.android_fab.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
<android.support.v7.widget.RecyclerView
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:buttonIcon="@drawable/ic_add_white_24dp"
|
||||
app:buttonBackgroundColour="@color/accent"/>
|
||||
android:id="@+id/list"
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</android.support.v7.widget.RecyclerView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="@string/devices_list_view_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="@{safeUnbox(isEmpty) ? View.VISIBLE : View.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" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -2,26 +2,69 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<FrameLayout
|
||||
<data>
|
||||
<variable
|
||||
name="isEmpty"
|
||||
type="Boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<variable
|
||||
name="listeningPortTaken"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/list"
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</ListView>
|
||||
<TextView
|
||||
android:id="@+id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/folder_list_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
android:layout_height="0dp">
|
||||
|
||||
</FrameLayout>
|
||||
<android.support.v7.widget.RecyclerView
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/list"
|
||||
android:visibility="@{safeUnbox(isEmpty) ? View.GONE : View.VISIBLE}" />
|
||||
|
||||
</layout>
|
||||
<TextView
|
||||
android:id="@+id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/folder_list_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="@{safeUnbox(isEmpty) ? View.VISIBLE : View.GONE}" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark"
|
||||
android:background="?colorPrimary"
|
||||
android:visibility="@{safeUnbox(listeningPortTaken) ? View.VISIBLE : View.GONE}"
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/other_syncthing_instance_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/other_syncthing_instance_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="listeningPortTaken"
|
||||
type="Boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
||||
<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" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{safeUnbox(listeningPortTaken) ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="#eee"
|
||||
android:text="@string/other_syncthing_instance_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginBottom="48dp"
|
||||
android:visibility="@{safeUnbox(listeningPortTaken) ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:textColor="#eee"
|
||||
android:text="@string/other_syncthing_instance_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
<ScrollView
|
||||
android:layout_gravity="center"
|
||||
android:id="@+id/scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<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" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/found_devices"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<!--
|
||||
Found device ids will be put here as buttons
|
||||
|
||||
This does not use an ListView or RecyclerView because this allows using
|
||||
wrap_content as height and because it's expected to be an small list
|
||||
-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</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>
|
||||
@@ -1,37 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<RelativeLayout
|
||||
<data>
|
||||
<variable
|
||||
name="name"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="status"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="isConnected"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:background="?selectableItemBackground"
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="12dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_laptop_green_24dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"/>
|
||||
tools:src="@drawable/ic_laptop_green_24dp"
|
||||
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/device_name"
|
||||
<View
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_alignParentTop="true"
|
||||
android:gravity="top"
|
||||
android:textAlignment="gravity"
|
||||
android:paddingStart="40dp"
|
||||
android:paddingEnd="40dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
tools:text="Computer"
|
||||
android:text="@{name}"
|
||||
android:id="@+id/device_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="gravity"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:text="@{status}"
|
||||
tools:text="Trying to connect to 127.0.0.1 ..."
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
|
||||
@@ -2,7 +2,21 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="fileName"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="fileSize"
|
||||
type="String" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="android.text.TextUtils" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
@@ -19,6 +33,8 @@
|
||||
android:layout_alignParentTop="true" />
|
||||
|
||||
<TextView
|
||||
tools:text="Test Directory"
|
||||
android:text="@{fileName}"
|
||||
android:id="@+id/file_label"
|
||||
android:maxLines="1"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -29,7 +45,9 @@
|
||||
android:textSize="22sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/file_size"
|
||||
android:visibility="@{TextUtils.isEmpty(fileSize) ? View.GONE : View.VISIBLE}"
|
||||
tools:text="250 MB"
|
||||
android:text="@{fileSize}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<data>
|
||||
<variable
|
||||
name="folderName"
|
||||
type="String" />
|
||||
|
||||
<RelativeLayout
|
||||
<variable
|
||||
name="lastModification"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="info"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="info2"
|
||||
type="String" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="android.text.TextUtils" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
@@ -10,39 +33,46 @@
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/folder_name"
|
||||
tools:text="Music"
|
||||
android:text="@{folderName}"
|
||||
android:id="@+id/folder_name_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_alignParentTop="true"
|
||||
android:gravity="top"
|
||||
android:textAlignment="gravity"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
tools:text="Last modified: two minutes ago"
|
||||
android:text="@{lastModification}"
|
||||
android:id="@+id/folder_lastmod_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_below="@id/folder_name"
|
||||
android:textSize="14sp"
|
||||
android:layout_alignParentStart="true" />
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
tools:text="Additional information"
|
||||
android:text="@{info}"
|
||||
android:id="@+id/folder_content_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_below="@id/folder_lastmod_info"
|
||||
android:textSize="14sp"
|
||||
android:layout_alignParentStart="true" />
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{TextUtils.isEmpty(info2) ? View.GONE : View.VISIBLE}"
|
||||
tools:text="Index Update Progress"
|
||||
android:text="@{info2}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
</layout>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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>
|
||||
@@ -13,6 +13,17 @@
|
||||
android:icon="@drawable/ic_laptop_gray_24dp"
|
||||
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" />
|
||||
|
||||
<item
|
||||
android:id="@+id/clear_index"
|
||||
android:icon="@drawable/ic_delete_gray_24dp"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
app:showAsAction="always"
|
||||
android:icon="@drawable/ic_home_white_24dp"
|
||||
android:id="@+id/go_home"
|
||||
android:title="@string/folder_browser_home" />
|
||||
</menu>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -1,12 +1,9 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
|
||||
<string name="folder_list_empty_message">Kein Ordner verfügbar</string>
|
||||
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
|
||||
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</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="invalid_device_id">Fehler: Ungültige Geräte ID</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>
|
||||
@@ -15,10 +12,11 @@
|
||||
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</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="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
|
||||
<string name="index_update_progress_label">Index Aktualisierung für Ordner %1$s, %2$d %% synchronisiert</string>
|
||||
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet...</string>
|
||||
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
|
||||
<string name="remove_device_title">Gerät entfernen:</string>
|
||||
<string name="remove_device_message">Gerät %1$s entfernen?</string>
|
||||
<string name="remove_device_title">Gerät %1$s entfernen?</string>
|
||||
<string name="remove_device_message">Gerät %1$s von der Liste der bekannten Geräte 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="folders_label">Ordner</string>
|
||||
@@ -26,4 +24,50 @@
|
||||
<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>
|
||||
<string name="show_device_id">Geräte ID anzeigen</string>
|
||||
<string name="device_id">Geräte ID</string>
|
||||
<string name="device_id_copied">Geräte ID in den Zwischenspeicher kopiert</string>
|
||||
<string name="share_device_id_chooser">Teile Geräte ID mit</string>
|
||||
<string name="other_syncthing_instance_title">Eine andere Syncthing-Instanz läuft bereits</string>
|
||||
<string name="other_syncthing_instance_message">Lokale Auffindung wird nicht funktionieren. Stoppen Sie die andere Syncthing Instanz, um die lokale Auffindung zu ermöglichen.</string>
|
||||
<string name="intro_page_one_title">Willkommen zu Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing ersetzt proprietäre Sync- und Cloud-Services durch etwas Offenes, Vertrauenswürdiges und Dezentrales. Ihre Daten sind allein Ihre Daten, und Sie verdienen es zu wählen, wo sie gespeichert werden, ob sie an Dritte weitergegeben werden und wie sie über das Internet übertragen werden.</string>
|
||||
<string name="intro_page_two_title">Ein Gerät hinzufügen</string>
|
||||
<string name="intro_page_three_title">Ordner teilen</string>
|
||||
<string name="intro_page_two_description">Eine Syncthing Geräte ID eingeben oder QR Code einer Geräte ID scannen.</string>
|
||||
<string name="intro_page_three_description">Akzeptieren Sie nun das Gerät mit der ID %1$s und geben Sie einen Ordner mit ihm frei. Es kann einige Minuten dauern, bis sich die Geräte verbinden.</string>
|
||||
<string name="intro_page_three_searching_device">Versuche das andere Gerät zu finden. Dies kann einen Moment dauern.</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="settings_app_version_title">App-Version</string>
|
||||
<string name="settings_local_device_name">Lokaler Gerätenamen</string>
|
||||
<string name="settings_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</string>
|
||||
<string name="settings_shutdown_delay_title">Ausschaltverzögerung</string>
|
||||
<string name="settings_force_stop">Beenden dieser App erzwingen</string>
|
||||
<string name="settings_last_error_title">Letzter Fehler</string>
|
||||
<string name="settings_last_error_summary">Details des letzten Fehlers anzeigen</string>
|
||||
<string name="settings_report_bug_title">Einen Fehler melden</string>
|
||||
<string name="settings_report_bug_summary">Den Bugtracker bei GitHub für diese App öffnen</string>
|
||||
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
|
||||
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 Sekunden</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 Sekunden</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 Minute</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 Minuten</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Aufgrund des Verhaltens dieser App und des Verhaltens des Syncthing-Servers können Sie sich für einige Minuten nicht verbinden, wenn die App erzwungen beendet wurde (durch das Entfernen aus der Liste der aktiven Apps) oder die Verbindung unterbrochen wurde.
|
||||
Dies gilt nicht für Verbindungen, die per lokaler Gerätesuche hergestellt wurden.</string>
|
||||
<string name="dialog_file_save_as">Speichern unter</string>
|
||||
<string name="pending_index_updates">%d Index-Aktualisierungen verbleibend</string>
|
||||
<string name="device_status_connecting">Verbinden mit %s</string>
|
||||
<string name="device_status_connected">Mit %s verbunden</string>
|
||||
<string name="device_status_disconnected">Verbinden wird bald erneut versucht - es sind %d Adressen bekannt</string>
|
||||
<string name="device_status_no_address">Keine bekannte Adresse für dieses Gerät</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Ordnersynchronisation für neues Gerät aktivieren</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Möchten Sie den Ordner %1$s mit %2$s (%3$s) synchronisieren?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Synchronisieren</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Nicht synchronisieren</string>
|
||||
<string name="dialog_folder_info_device_list">Ordner teilen mit:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Etwas in Syncthing Lite hat nicht funktioniert. Sie können die Details in den Einstellungen von Syncthing Lite anzeigen.</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Carpeta no disponible</string>
|
||||
<string name="clear_local_cache_index_label">Limpia caché/índice local</string>
|
||||
<string name="devices_list_view_empty_message">Dispositivos no disponibles</string>
|
||||
<string name="invalid_device_id">Error: identificador de dispositivo inválido</string>
|
||||
<string name="dialog_downloading_file">Descargando archivo%1$s</string>
|
||||
<string name="toast_file_download_failed">Falló al descargar el archivo</string>
|
||||
<string name="toast_open_file_failed">No se ha encontrado una app compatible</string>
|
||||
<string name="toast_file_upload_failed">Error en la carga de archivos
|
||||
</string>
|
||||
<string name="toast_upload_complete">Carga de archivos completa
|
||||
</string>
|
||||
<string name="dialog_uploading_file">Cargando archivo %1$s</string>
|
||||
<string name="clear_cache_and_index_title">¿Borrar la caché local y el índice?</string>
|
||||
<string name="clear_cache_and_index_body">¿Borrar todos los datos de la caché local y los datos de índice?</string>
|
||||
<string name="index_update_progress_label">Actualización del índice de las carpetas %1$s, %2$d%% sincronizado</string>
|
||||
<string name="loading_config_starting_syncthing_client">Cargando configuración, iniciando el cliente de sincronización....</string>
|
||||
<string name="last_modified_time">Última modificación: %1$s</string>
|
||||
<string name="remove_device_title">¿Quitar dispositivo %1$s?</string>
|
||||
<string name="remove_device_message">¿Quitar dispositivo %1$s de la lista de dispositivos conocidos?</string>
|
||||
<string name="device_import_success">Dispositivo %1$s importado con éxito</string>
|
||||
<string name="device_already_known">Dispositivo %1$s ya está presente</string>
|
||||
<string name="folders_label">Carpetas</string>
|
||||
<string name="devices_label">Dispositivos</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d archivos, %3$ddirectorios</string>
|
||||
<string name="file_info">%1$s, modificado por última vez %2$s</string>
|
||||
<string name="show_device_id">Mostrar ID de dispositivo </string>
|
||||
<string name="device_id">ID de dispositivo</string>
|
||||
<string name="device_id_copied">ID de dispositivo copiado al portapapeles</string>
|
||||
<string name="share_device_id_chooser">Compartir ID de dispositivo con</string>
|
||||
<string name="other_syncthing_instance_title">Otra instancia de Syncthing está siendo ejecutada</string>
|
||||
<string name="other_syncthing_instance_message">El descubrimiento local no funcionará. Detenga la otra instancia de Syncthing para habilitar la detección local.</string>
|
||||
<string name="intro_page_one_title">Bienvenido a Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing reemplaza los servicios de sincronización y nube propietarios por algo abierto, fiable y descentralizado. Sus datos son sólo suyos y usted merece elegir dónde se almacenan, si se comparten con terceros y cómo se transmiten a través de Internet.</string>
|
||||
<string name="intro_page_two_title">Añadir un dispositivo</string>
|
||||
<string name="intro_page_three_title">Compartir tus carpetas</string>
|
||||
<string name="intro_page_two_description">Introduce un ID de dispositivo de Syncthing o escanea un ID de dispositivo desde un código QR</string>
|
||||
<string name="intro_page_three_description">Acepta ahora el dispositivo con ID %1$s, y comparte una carpeta con él. Pueden pasar unos minutos hasta que los dispositivos se conecten.</string>
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="settings_app_version_title">Versión de la aplicación</string>
|
||||
<string name="settings_local_device_name">Nombre del dispositivo local</string>
|
||||
<string name="settings_local_device_summary">El nombre que otros dispositivos verán para este dispositivo</string>
|
||||
<string name="settings_shutdown_delay_title">Retardo en el apagado</string>
|
||||
<string name="device_id_dialog_title">Introducir la ID del dispositivo</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 segundos</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 segundos</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutos</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
@@ -3,10 +3,7 @@
|
||||
<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="invalid_device_id">ID de l\'appareil invalide</string>
|
||||
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="invalid_device_id">Erreur: ID de l\'appareil invalide</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>
|
||||
@@ -15,7 +12,8 @@
|
||||
<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="loading_config_starting_syncthing_client">Chargement de la configuration, démarrage du client Syncthing</string>
|
||||
<string name="index_update_progress_label">Mise à jour de l\'index pour le dossier %1$s, %2$d%% synchronisés</string>
|
||||
<string name="loading_config_starting_syncthing_client">Chargement de la configuration, démarrage du client Syncthing...</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="remove_device_message">Supprimer l\'appareil %1$s de la liste des appareil connus ?</string>
|
||||
@@ -26,4 +24,38 @@
|
||||
<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>
|
||||
<string name="show_device_id">Afficher l\'ID de l\'appareil</string>
|
||||
<string name="device_id">ID de l\'appareil</string>
|
||||
<string name="device_id_copied">ID de l\'appareil copié dans le presse-papier</string>
|
||||
<string name="share_device_id_chooser">Partager l\'ID de l\'appareil avec</string>
|
||||
<string name="other_syncthing_instance_title">Une autre instance de Syncthing fonctionne</string>
|
||||
<string name="other_syncthing_instance_message">La découverte locale ne fonctionnera pas. Arrêtez l\'autre instance de Syncthing pour activer la découverte locale.</string>
|
||||
<string name="intro_page_one_title">Bienvenue dans Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing remplace les services de synchronisation et de cloud propriétaires par quelque chose d\'ouvert, fiable et décentralisé. Vos données sont uniquement vos données et vous méritez de choisir où elles sont stockées, si elles sont partagées avec des tiers et comment elles sont transmises sur Internet.</string>
|
||||
<string name="intro_page_two_title">Ajouter un appareil</string>
|
||||
<string name="intro_page_three_title">Partager vos dossiers</string>
|
||||
<string name="intro_page_two_description">Entrer l\'ID Syncthing de l\'appareil, ou scanner le QR code de l\'ID d\'un appareil.</string>
|
||||
<string name="intro_page_three_description">Maintenant, acceptez l\'appareil avec l\'ID %1$s, et partagez un dossier avec lui. Cela peut prendre quelques minutes avant que les appareils ne se connectent.</string>
|
||||
<string name="intro_page_three_searching_device">Essayer de trouver l\'autre appareil. Cela peut prendre un moment.</string>
|
||||
<string name="settings">Réglages</string>
|
||||
<string name="settings_app_version_title">Version d\'application</string>
|
||||
<string name="settings_local_device_name">Nom local de l\'appareil</string>
|
||||
<string name="settings_local_device_summary">Le nom que les autres appareils verront pour cet appareil</string>
|
||||
<string name="settings_shutdown_delay_title">Délai d\'arrêt</string>
|
||||
<string name="settings_force_stop">Forcer l\'arrêt de cette application</string>
|
||||
<string name="settings_last_error_title">Dernière erreur</string>
|
||||
<string name="settings_last_error_summary">Voir les détails de la dernière erreur</string>
|
||||
<string name="settings_report_bug_title">Rapporter un bug</string>
|
||||
<string name="settings_report_bug_summary">Ouvrir un incident pour cette application sur GitHub</string>
|
||||
<string name="copy_to_clipboard">Copier dans le presse-papiers</string>
|
||||
<string name="copied_to_clipboard">Copié dans le presse-papiers</string>
|
||||
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secondes</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secondes</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minute</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
|
||||
<string name="dialog_file_save_as">Enregistrer sous</string>
|
||||
<string name="pending_index_updates">%d mises à jour d\'index en attente</string>
|
||||
<string name="device_status_connecting">Connexion à %s</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Nincs elérhető mappa</string>
|
||||
<string name="clear_local_cache_index_label">Helyi gyorsítótár/index törlése</string>
|
||||
<string name="devices_list_view_empty_message">Nincs elérhető eszköz</string>
|
||||
<string name="invalid_device_id">Hiba: helytelen eszközazonosítő</string>
|
||||
<string name="dialog_downloading_file">Fájl letöltése %1$s</string>
|
||||
<string name="toast_file_download_failed">Hiba a fájl letöltése közben</string>
|
||||
<string name="toast_open_file_failed">Nem található kompatibilis alkalmazás</string>
|
||||
<string name="toast_file_upload_failed">Hiba fájl feltöltése közben</string>
|
||||
<string name="toast_upload_complete">Feltöltés befejezve</string>
|
||||
<string name="dialog_uploading_file">Fájl feltöltése: %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Törlöd a helyi gyorsítótárat és index-et?</string>
|
||||
<string name="clear_cache_and_index_body">Törlöd az összes helyi gyorsítótár-at és index-et?</string>
|
||||
<string name="index_update_progress_label">Index frissítés szinkronizálva a %1$s,%2$d,%% mappákhoz</string>
|
||||
<string name="loading_config_starting_syncthing_client">Beállítások betöltése, Syncthing kliens indítása</string>
|
||||
<string name="last_modified_time">Utolsó módosítás: %1$s</string>
|
||||
<string name="remove_device_title">Törlöd a %1$s eszközt?</string>
|
||||
<string name="remove_device_message">Törlöd a %1$s eszközt az ismertek listájáról?</string>
|
||||
<string name="device_import_success">Eszköz sikeresen importálva: %1$s</string>
|
||||
<string name="device_already_known">%1$s eszköz már hozzá van adva</string>
|
||||
<string name="folders_label">Mappák</string>
|
||||
<string name="devices_label">Eszközök</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d fájlok, %3$d mappák</string>
|
||||
<string name="file_info">%1$s, utoljára módosítva %2$s</string>
|
||||
<string name="show_device_id">Eszközazonosító megjelenítése</string>
|
||||
<string name="device_id">Eszközazonosító</string>
|
||||
<string name="device_id_copied">Eszközazonosító a vágólapra másolva</string>
|
||||
<string name="share_device_id_chooser">Eszközazonosító megosztása</string>
|
||||
<string name="other_syncthing_instance_title">Egy másik Syncthing folyamat fut</string>
|
||||
<string name="other_syncthing_instance_message">Helyi felfedezés nem fog működni. Állítsd le a másik Syncthing folyamatot a helyi felfedezés bekapcsolásához.</string>
|
||||
<string name="intro_page_one_title">Üdvözöllek a Syncthing Lite-ban</string>
|
||||
<string name="intro_page_one_description">A Syncthing a zárt forrású szinkronizáló és felhő szolgáltatásokat egy nyílt, megbízható és decentralizált szoftverrel váltja fel. A te adatod csak a tiéd és te szabod meg, hogy hol tárolod, kivel osztod meg, és hogyan továbbítod az interneten.</string>
|
||||
<string name="intro_page_two_title">Eszköz hozzáadása</string>
|
||||
<string name="intro_page_three_title">Mappáid megosztása</string>
|
||||
<string name="intro_page_two_description">Adj meg egy Syncthing eszközazonosítót, vagy olvasd be QR kódból</string>
|
||||
<string name="intro_page_three_description">Fogadd el a(z) %1$s azonosítójú eszközt, és ossz meg vele egy mappát. Beletelhet néhány percbe mire az eszközök csatlakoznak.</string>
|
||||
<string name="settings">Beállítások</string>
|
||||
<string name="settings_app_version_title">Verzió</string>
|
||||
<string name="settings_local_device_name">Helyi eszköz neve</string>
|
||||
<string name="settings_local_device_summary">Név amit a többi eszköz fog látni</string>
|
||||
<string name="settings_shutdown_delay_title">Leállítás késleltetés</string>
|
||||
<string name="device_id_dialog_title">Eszközazonosító megadása</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 másodperc</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 perc</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
@@ -3,11 +3,8 @@
|
||||
<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="invalid_device_id">ID dispositivo non valido</string>
|
||||
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
|
||||
<string name="dialog_downloading_file">Download del file %1$s</string>
|
||||
<string name="invalid_device_id">Errore: ID dispositivo non valido</string>
|
||||
<string name="dialog_downloading_file">Scaricamento 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>
|
||||
@@ -15,10 +12,11 @@
|
||||
<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="loading_config_starting_syncthing_client">Caricamento configurazione, avvio del client syncthing</string>
|
||||
<string name="index_update_progress_label">Aggiornamento dell\'indice per la cartella %1$s, %2$d%% sincronizzato</string>
|
||||
<string name="loading_config_starting_syncthing_client">Caricamento configurazione, avvio del client syncthing...</string>
|
||||
<string name="last_modified_time">Ultima modifica: %1$s</string>
|
||||
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</string>
|
||||
<string name="remove_device_message">Rimuovere il dispositivo %1$s dalla lista dei dispositivi noti?</string>
|
||||
<string name="remove_device_message">Rimuovere %1$s dalla lista dei dispositivi noti?</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>
|
||||
@@ -26,4 +24,54 @@
|
||||
<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>
|
||||
<string name="show_device_id">Mostra l\'ID del dispositivo</string>
|
||||
<string name="device_id">ID dispositivo</string>
|
||||
<string name="device_id_copied">ID dispositivo copiato negli appunti</string>
|
||||
<string name="share_device_id_chooser">Condividi ID dispositivo con</string>
|
||||
<string name="other_syncthing_instance_title">Un\'altra istanza di Syncthing è in esecuzione</string>
|
||||
<string name="other_syncthing_instance_message">L\'individuazione locale non funzionerà. Arresta l\'altra istanza di Syncthing per abilitare l\'individuazione locale.</string>
|
||||
<string name="intro_page_one_title">Benvenuto in Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing sostituisce servizi proprietari di sincronizzazione e cloud con qualcosa di aperto, affidabile e decentralizzato. I tuoi dati sono solo tuoi e meriti di scegliere dove vengono immagazzinati, se sono condivisi con terze parti e come sono trasmessi attraverso Internet.</string>
|
||||
<string name="intro_page_two_title">Aggiungi un dispositivo</string>
|
||||
<string name="intro_page_three_title">Condividi le tue cartelle</string>
|
||||
<string name="intro_page_two_description">Immetti un ID dispositivo Syncthing o esegui la scansione di un ID dispositivo da un codice QR</string>
|
||||
<string name="intro_page_three_description">Ora accetta il dispositivo con ID %1$s, e condividi una cartella con esso. Potrebbero essere necessari alcuni minuti prima che i dispositivi si connettano.</string>
|
||||
<string name="intro_page_three_searching_device">In attesa di trovare altri dispositivi. Questo potrebbe richiedere un momento.</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
<string name="settings_app_version_title">Versione dell\'app</string>
|
||||
<string name="settings_local_device_name">Nome del dispositivo locale</string>
|
||||
<string name="settings_local_device_summary">Il nome che altri dispositivi vedranno per questo dispositivo</string>
|
||||
<string name="settings_shutdown_delay_title">Ritardo di chiusura</string>
|
||||
<string name="settings_shutdown_delay_summary">Tempo prima della chiusura del client Syncthing dopo l\'ultimo utilizzo</string>
|
||||
<string name="settings_force_stop">Forza l\'arresto dell\'App</string>
|
||||
<string name="settings_last_error_title">Ultimo errore</string>
|
||||
<string name="settings_last_error_summary">Visualizza i dettagli dell\'ultimo errore</string>
|
||||
<string name="settings_report_bug_title">Segnala un bug</string>
|
||||
<string name="settings_report_bug_summary">Apri segnalazioni per quest\'App su GitHub</string>
|
||||
<string name="copy_to_clipboard">Copia negli appunti</string>
|
||||
<string name="copied_to_clipboard">Copiato negli appunti</string>
|
||||
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secondi</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secondi</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minuti</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
A causa del comportamento di quest\'App e del comportamento del Server Syncthing,
|
||||
non è possibile riconnettersi per alcuni minuti se l\'app è stata chiusa (a causa della rimozione dall\'elenco app recenti)
|
||||
o la connessione è stata interrotta.
|
||||
Questo non si applica alle connessioni di individuazione locale
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Salva come</string>
|
||||
<string name="pending_index_updates">%d aggiornamenti degli indici in sospeso</string>
|
||||
<string name="device_status_connecting">Connessione a %s</string>
|
||||
<string name="device_status_connected">Connesso a %s</string>
|
||||
<string name="device_status_disconnected">Nuovo tentativo di connessione a breve: ci sono %d indirizzi noti</string>
|
||||
<string name="device_status_no_address">Nessun indirizzo conosciuto per il dispositivo</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Abilita sincronizzazione cartella per il nuovo dispositivo</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Vuoi sincronizzare %1$s con %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Sincronizzare</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Non sincronizzare</string>
|
||||
<string name="dialog_folder_info_device_list">Condividi la cartella con:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Qualcosa non ha funzionato in Syncthing Lite. È possibile visualizzare i dettagli dalle impostazioni di Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
<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="invalid_device_id">デバイス ID が無効です</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
<string name="invalid_device_id">エラー: デバイス ID が無効です</string>
|
||||
<string name="dialog_downloading_file">ファイル %1$s のダウンロード中</string>
|
||||
<string name="toast_file_download_failed">ファイルのダウンロードに失敗しました</string>
|
||||
<string name="toast_open_file_failed">利用できるアプリが見つかりません</string>
|
||||
@@ -15,10 +12,11 @@
|
||||
<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="loading_config_starting_syncthing_client">設定の読み込み中、syncthing クライアントの開始中</string>
|
||||
<string name="index_update_progress_label">フォルダー %1$s の索引を更新しました。 %2$d%% 同期しました</string>
|
||||
<string name="loading_config_starting_syncthing_client">設定の読み込み中、syncthing クライアントの開始中…</string>
|
||||
<string name="last_modified_time">最終更新: %1$s</string>
|
||||
<string name="remove_device_title">デバイス %1$sを削除しますか?</string>
|
||||
<string name="remove_device_message">既存のデバイスリストからデバイス %1$s を削除しますか?</string>
|
||||
<string name="remove_device_message">既存のデバイスのリストから %1$s を削除しますか?</string>
|
||||
<string name="device_import_success">デバイス %1$s のインポートに成功しました</string>
|
||||
<string name="device_already_known">デバイスは既に存在します %1$s</string>
|
||||
<string name="folders_label">フォルダー</string>
|
||||
@@ -26,4 +24,22 @@
|
||||
<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>
|
||||
<string name="show_device_id">デバイス ID を表示</string>
|
||||
<string name="device_id">デバイス ID</string>
|
||||
<string name="device_id_copied">デバイス ID をクリップボードにコピーしました</string>
|
||||
<string name="share_device_id_chooser">次とデバイス ID を共有</string>
|
||||
<string name="other_syncthing_instance_title">別の Syncthing インスタンスが実行中です</string>
|
||||
<string name="other_syncthing_instance_message">ローカルの探索は動作しません。他の Syncthing インスタンスを停止して、ローカルの探索を有効にしてください。</string>
|
||||
<string name="intro_page_one_title">Syncthing Lite へようこそ</string>
|
||||
<string name="intro_page_one_description">Syncthing は、プロプライエタリな同期およびクラウドサービスを、オープンで信頼性があり、分散化されたものに置き換えます。 あなたのデータはあなただけのものであり、それが第三者と共有され、インターネット経由で送信される場合、保存される場所を選択する必要があります。</string>
|
||||
<string name="intro_page_two_title">デバイスを追加</string>
|
||||
<string name="intro_page_three_title">フォルダーを共有</string>
|
||||
<string name="intro_page_two_description">Syncthing デバイス ID を入力、または QR コードからデバイス ID をスキャンしてください</string>
|
||||
<string name="intro_page_three_description">ID %1$s のデバイスを承認して、フォルダーを共有しました。デバイスが接続されるまで数分かかることがあります。</string>
|
||||
<string name="settings">設定</string>
|
||||
<string name="settings_app_version_title">アプリバージョン</string>
|
||||
<string name="settings_local_device_name">ローカルのデバイス名</string>
|
||||
<string name="settings_local_device_summary">他のデバイスがこのデバイスを表示する名前</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Geen map beschikbaar</string>
|
||||
<string name="clear_local_cache_index_label">Lokaal cachegeheugen/index wissen</string>
|
||||
<string name="devices_list_view_empty_message">Geen apparaten beschikbaar</string>
|
||||
<string name="invalid_device_id">Fout: ongeldigen apparaats-ID</string>
|
||||
<string name="dialog_downloading_file">Bestand %1$s wordt gedownload</string>
|
||||
<string name="toast_file_download_failed">Download van bestand mislukt</string>
|
||||
<string name="toast_open_file_failed">Gene compatibelen app gevonden</string>
|
||||
<string name="toast_file_upload_failed">Uploaden van bestand mislukt</string>
|
||||
<string name="toast_upload_complete">Uploaden van bestand voltooid</string>
|
||||
<string name="dialog_uploading_file">Bestand %1$s wordt geüpload</string>
|
||||
<string name="clear_cache_and_index_title">Lokaal cachegeheugen en index wissen?</string>
|
||||
<string name="clear_cache_and_index_body">Alle lokale cache- en indexgegevens wissen?</string>
|
||||
<string name="index_update_progress_label">Index voor map %1$s wordt bijgewerkt, %2$d%% gesynchroniseerd</string>
|
||||
<string name="loading_config_starting_syncthing_client">Configuratie wordt geladen, Syncthing-cliënt wordt opgestart…</string>
|
||||
<string name="last_modified_time">Laatst gewijzigd: %1$s</string>
|
||||
<string name="remove_device_title">Apparaat %1$s verwijderen?</string>
|
||||
<string name="remove_device_message">%1$s verwijderen uit de lijst van gekende apparaten?</string>
|
||||
<string name="device_import_success">Apparaat %1$s geïmporteerd</string>
|
||||
<string name="device_already_known">Apparaat %1$s reeds aanwezig</string>
|
||||
<string name="folders_label">Mappen</string>
|
||||
<string name="devices_label">Apparaten</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d bestanden, %3$d mappen</string>
|
||||
<string name="file_info">%1$s, laatst gewijzigd %2$s</string>
|
||||
<string name="show_device_id">Apparaats-ID tonen</string>
|
||||
<string name="device_id">Apparaats-ID</string>
|
||||
<string name="device_id_copied">Apparaats-ID gekopieerd naar klembord</string>
|
||||
<string name="share_device_id_chooser">Apparaats-ID delen met</string>
|
||||
<string name="other_syncthing_instance_title">Een andere Syncthing-instantie wordt reeds uitgevoerd</string>
|
||||
<string name="other_syncthing_instance_message">Lokale ontdekking gaat niet werken. Stopt de andere Syncthing-instantie voor lokale ontdekking in te schakelen.</string>
|
||||
<string name="intro_page_one_title">Welkom bij Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing vervangt niet-vrije synchronisatie- en clouddiensten door iets opens, betrouwbaars en gedecentraliseerds. Uw gegevens behoren enkel u toe en gij bepaalt waar dat ze worden opgeslagen, of dat ze worden gedeeld met een derde partij en hoe dat ze over het internet worden verzonden.</string>
|
||||
<string name="intro_page_two_title">Voegt een apparaat toe</string>
|
||||
<string name="intro_page_three_title">Deelt uw mappen</string>
|
||||
<string name="intro_page_two_description">Voert ne Syncthing-apparaats-ID in, of scant nen apparaats-ID van een QR-code</string>
|
||||
<string name="intro_page_three_description">Aanvaard nu het apparaat met ID %1$s, en deelt er een map mee. Het kan enkele minuten duren vooraleer dat de apparaten verbinding maken.</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="settings_app_version_title">Appversie</string>
|
||||
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
|
||||
<string name="settings_local_device_summary">De naam die dat andere apparaten voor dit apparaat gaan zien</string>
|
||||
<string name="device_id_dialog_title">Voert nen apparaats-ID in</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,45 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Geen map beschikbaar</string>
|
||||
<string name="clear_local_cache_index_label">Lokale cache/index wissen</string>
|
||||
<string name="devices_list_view_empty_message">Geen apparaten beschikbaar</string>
|
||||
<string name="invalid_device_id">Fout: ongeldige apparaats-ID</string>
|
||||
<string name="dialog_downloading_file">Bestand %1$s wordt gedownload</string>
|
||||
<string name="toast_file_download_failed">Download van bestand mislukt</string>
|
||||
<string name="toast_open_file_failed">Geen compatibele app gevonden</string>
|
||||
<string name="toast_file_upload_failed">Uploaden van bestand mislukt</string>
|
||||
<string name="toast_upload_complete">Uploaden van bestand voltooid</string>
|
||||
<string name="dialog_uploading_file">Bestand %1$s wordt geüpload</string>
|
||||
<string name="clear_cache_and_index_title">Lokale cache en index wissen?</string>
|
||||
<string name="clear_cache_and_index_body">Alle lokale cache- en indexgegevens wissen?</string>
|
||||
<string name="index_update_progress_label">Index voor map %1$s wordt bijgewerkt, %2$d%% gesynchroniseerd</string>
|
||||
<string name="loading_config_starting_syncthing_client">Configuratie wordt geladen, Syncthing-cliënt wordt opgestart…</string>
|
||||
<string name="last_modified_time">Laatst gewijzigd: %1$s</string>
|
||||
<string name="remove_device_title">Apparaat %1$s verwijderen?</string>
|
||||
<string name="remove_device_message">%1$s verwijderen uit de lijst van gekende apparaten?</string>
|
||||
<string name="device_import_success">Apparaat %1$s geïmporteerd</string>
|
||||
<string name="device_already_known">Apparaat %1$s reeds aanwezig</string>
|
||||
<string name="folders_label">Mappen</string>
|
||||
<string name="devices_label">Apparaten</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d bestanden, %3$d mappen</string>
|
||||
<string name="file_info">%1$s, laatst gewijzigd %2$s</string>
|
||||
<string name="show_device_id">Apparaats-ID tonen</string>
|
||||
<string name="device_id">Apparaats-ID</string>
|
||||
<string name="device_id_copied">Apparaats-ID gekopieerd naar klembord</string>
|
||||
<string name="share_device_id_chooser">Apparaats-ID delen met</string>
|
||||
<string name="other_syncthing_instance_title">Een andere Syncthing-instantie wordt reeds uitgevoerd</string>
|
||||
<string name="other_syncthing_instance_message">Lokale ontdekking zal niet werken. Stop de andere Syncthing-instantie om lokale ontdekking in te schakelen.</string>
|
||||
<string name="intro_page_one_title">Welkom bij Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing vervangt niet-vrije synchronisatie- en clouddiensten door iets opens, betrouwbaars en gedecentraliseerds. Je gegevens behoren enkel jou toe en jij bepaalt waar ze worden opgeslagen, of ze gedeeld worden met een derde partij en hoe ze over het internet verstuurd worden.</string>
|
||||
<string name="intro_page_two_title">Voeg een apparaat toe</string>
|
||||
<string name="intro_page_three_title">Deel je mappen</string>
|
||||
<string name="intro_page_two_description">Voer een Syncthing-apparaats-ID in, of scan een apparaats-ID van een QR-code</string>
|
||||
<string name="intro_page_three_description">Accepteer nu het apparaat met ID %1$s, en deel er een map mee. Het kan enkele minuten duren voordat de apparaten verbinden.</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="settings_app_version_title">Appversie</string>
|
||||
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
|
||||
<string name="settings_local_device_summary">De naam die andere apparaten voor dit apparaat zullen zien</string>
|
||||
<string name="device_id_dialog_title">Voer een apparaats-ID in</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -3,10 +3,7 @@
|
||||
<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="invalid_device_id">ID dispozitiv invalid</string>
|
||||
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
|
||||
<string name="invalid_device_id">Eroare: ID dispozitiv invalid</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>
|
||||
@@ -15,10 +12,11 @@
|
||||
<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="index_update_progress_label">Actualizarea indexul pentru directorul %1$s, %2$d %% sincronizat</string>
|
||||
<string name="loading_config_starting_syncthing_client">Încărcare setări, pornire client syncthing…</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="remove_device_message">Se va șterge %1$s din lista dispozitivelor cunoscute?</string>
|
||||
<string name="remove_device_message">Șterge %1$s din lista dispozitivelor cunoscute?</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>
|
||||
@@ -26,4 +24,58 @@
|
||||
<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>
|
||||
<string name="show_device_id">Arată ID dispozitiv</string>
|
||||
<string name="device_id">ID dispozitiv</string>
|
||||
<string name="device_id_copied">ID dispozitiv copiat în memorie</string>
|
||||
<string name="share_device_id_chooser">Partajează ID dispozitiv cu</string>
|
||||
<string name="other_syncthing_instance_title">O altă instanță de Syncthing rulează</string>
|
||||
<string name="other_syncthing_instance_message">Descoperire locală nu va funcționa. Opriți cealaltă instanță de Syncthing pentru a activa descoperirea locală.</string>
|
||||
<string name="intro_page_one_title">Bine ați venit la Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing înlocuiește serviciile proprietare de sincronizare și stocare
|
||||
tip cloud cu ceva deschis, de încredere și descentralizat. Datele
|
||||
dumneavoastră vă aparțin în totalitate și meritați să decideți unde vor
|
||||
fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
|
||||
trimise prin Internet.</string>
|
||||
<string name="intro_page_two_title">Adaugă un dispozitiv</string>
|
||||
<string name="intro_page_three_title">Partajați-vă directoarele</string>
|
||||
<string name="intro_page_two_description">Introduceți ID-ul Syncthing al unui dispozitiv sau scanați ID-ul unui dispozitiv dintr-un cod QR</string>
|
||||
<string name="intro_page_three_description">Acceptați acum dispozitivul cu ID-ul %1$s, și partajați un director cu el. S-ar putea să dureze câteva minute până când dispozitivele se vor conecta.</string>
|
||||
<string name="intro_page_three_searching_device">Se încearcă găsirea celuilalt dispozitiv. Această operație poate dura un moment.</string>
|
||||
<string name="settings">Setări</string>
|
||||
<string name="settings_app_version_title">Versiune aplicație</string>
|
||||
<string name="settings_local_device_name">Nume local dispozitiv</string>
|
||||
<string name="settings_local_device_summary">Numele pe care celălalt dispozitiv îl va vedea pentru acest dispozitiv</string>
|
||||
<string name="settings_shutdown_delay_title">Temporizare oprire</string>
|
||||
<string name="settings_shutdown_delay_summary">După cât timp se va închide clientul Syncthing în funcție de ultima utilizare</string>
|
||||
<string name="settings_force_stop">Forțează oprirea acestei aplicații</string>
|
||||
<string name="settings_last_error_title">Ultima eroare</string>
|
||||
<string name="settings_last_error_summary">Arată detaliile ultimei erori</string>
|
||||
<string name="settings_report_bug_title">Raportează o eroare</string>
|
||||
<string name="settings_report_bug_summary">Deschideți un raport de eroare pentru această aplicație pe GitHub</string>
|
||||
<string name="copy_to_clipboard">Copiază în memorie</string>
|
||||
<string name="copied_to_clipboard">Copiat în memorie</string>
|
||||
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secunde</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secunde</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minut</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minute</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Datorită modului în care această aplicație și serverul Syncthing funcționează,
|
||||
nu se poate face reconectarea timp de câteva minute după ce aplicația a fost oprită (ștearsă din lista de aplicații care rulează)
|
||||
sau conexiunea a fost întreruptă.
|
||||
Aceasta limitare nu se aplica la conexiunile descoperite local.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Salvează ca</string>
|
||||
<string name="pending_index_updates">%d actualizări de index în așteptare</string>
|
||||
<string name="device_status_connecting">Conectare la %s</string>
|
||||
<string name="device_status_connected">Conectat la %s</string>
|
||||
<string name="device_status_disconnected">Se va încerca conectarea în curând - există %d adrese cunoscute</string>
|
||||
<string name="device_status_no_address">Nici o adresă cunoscută pentru acest dispozitiv</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Activați sincronizarea directorului pentru un dispozitiv nou</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Doriți să sincronizați %1$s cu %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Se sincronizează</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Nu se sincronizează</string>
|
||||
<string name="dialog_folder_info_device_list">Partajează directorul cu:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">O eroare s-a produs in Syncthing Lite. Puteți vedea detalii în setările Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Ingen mapp tillgänglig</string>
|
||||
<string name="clear_local_cache_index_label">Rensa lokala cache/index</string>
|
||||
<string name="devices_list_view_empty_message">Inga enheter tillgängliga</string>
|
||||
<string name="invalid_device_id">Fel: Ogiltigt enhets-ID</string>
|
||||
<string name="dialog_downloading_file">Hämtar filen %1$s</string>
|
||||
<string name="toast_file_download_failed">Misslyckades med att hämta filen</string>
|
||||
<string name="toast_open_file_failed">Ingen kompatibel app hittades</string>
|
||||
<string name="toast_file_upload_failed">Filöverföring misslyckades</string>
|
||||
<string name="toast_upload_complete">Filöverföring färdig</string>
|
||||
<string name="dialog_uploading_file">Överför filen %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Rensa cache och index lokalt?</string>
|
||||
<string name="clear_cache_and_index_body">Rensa all cachedata och indexdata lokalt?</string>
|
||||
<string name="index_update_progress_label">Index uppdatering för mappen %1$s, %2$d%% synkroniserad</string>
|
||||
<string name="loading_config_starting_syncthing_client">Läser in konfiguration, starta synkroniserande klient...</string>
|
||||
<string name="last_modified_time">Senast ändrad: %1$s</string>
|
||||
<string name="remove_device_title">Ta bort enheten %1$s\?</string>
|
||||
<string name="remove_device_message">Ta bort %1$s från listan över kända enheter?</string>
|
||||
<string name="device_import_success">Importerad enheten %1$s</string>
|
||||
<string name="device_already_known">Enhet som redan finns %1$s</string>
|
||||
<string name="folders_label">Mappar</string>
|
||||
<string name="devices_label">Enheter</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d filer, %3$d kataloger</string>
|
||||
<string name="file_info">%1$s, senast ändrad %2$s</string>
|
||||
<string name="show_device_id">Visa enhets-ID</string>
|
||||
<string name="device_id">Enhets-ID</string>
|
||||
<string name="device_id_copied">Enhets-ID kopierad till urklipp</string>
|
||||
<string name="share_device_id_chooser">Dela enhets-ID med</string>
|
||||
<string name="other_syncthing_instance_title">En annan Syncthing-instans körs</string>
|
||||
<string name="other_syncthing_instance_message">Lokal upptäckt kommer inte att fungera. Stoppa den andra Syncthing-instansen för att möjliggöra lokal upptäckt.</string>
|
||||
<string name="intro_page_one_title">Välkommen till Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing ersätter proprietära synkroniserings- och molntjänster med något öppet, pålitligt och decentraliserat. Din data är endast din data och du förtjänar att välja var den lagras, om den delas med en tredjepart och hur den överförs över Internet.</string>
|
||||
<string name="intro_page_two_title">Lägg till en enhet</string>
|
||||
<string name="intro_page_three_title">Dela dina mappar</string>
|
||||
<string name="intro_page_two_description">Ange ett Syncthing enhets-ID, eller skanna ett enhets-ID-nummer från en QR-kod</string>
|
||||
<string name="intro_page_three_description">Acceptera nu enheten med ID %1$s och dela en mapp med den. Det kan ta några minuter tills enheterna ansluter.</string>
|
||||
<string name="intro_page_three_searching_device">Försöker hitta den andra enheten. Det kan ta ett ögonblick.</string>
|
||||
<string name="settings">Inställningar</string>
|
||||
<string name="settings_app_version_title">Appversion</string>
|
||||
<string name="settings_local_device_name">Lokala enhetens namn</string>
|
||||
<string name="settings_local_device_summary">Namnet som andra enheter kommer att se för den här enheten</string>
|
||||
<string name="settings_shutdown_delay_title">Avstängningsfördröjning</string>
|
||||
<string name="settings_force_stop">Tvinga stoppa denna App</string>
|
||||
<string name="settings_last_error_title">Senaste felet</string>
|
||||
<string name="settings_last_error_summary">Visa detaljerna för det senaste felet</string>
|
||||
<string name="settings_report_bug_title">Rapportera ett fel</string>
|
||||
<string name="settings_report_bug_summary">Öppna problemen för den här appen på GitHub</string>
|
||||
<string name="copy_to_clipboard">Kopiera till urklipp</string>
|
||||
<string name="copied_to_clipboard">Kopieras till urklippet</string>
|
||||
<string name="device_id_dialog_title">Ange enhets-ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 sekunder</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 sekunder</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minut</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minuter</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
På grund av beteendet hos denna App och beteendet hos Syncthing-servern,
|
||||
du kan inte återansluta i några minuter om appen dödades (på grund av att du tog bort från den senaste applistan)
|
||||
eller anslutningen avbröts.
|
||||
Detta gäller inte lokala upptäcktsanslutningar.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Spara som</string>
|
||||
<string name="pending_index_updates">%d indexuppdateringar som väntar</string>
|
||||
<string name="device_status_connecting">Ansluter till %s</string>
|
||||
<string name="device_status_connected">Ansluten till %s</string>
|
||||
<string name="device_status_disconnected">Kommer att försöka ansluta snart - det finns%d kända adresser</string>
|
||||
<string name="device_status_no_address">Ingen känd adress för enheten</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Aktivera mappsynkronisering för ny enhet</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Vill du synkronisera %1$s med %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Synkronisera</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Synkronisera inte</string>
|
||||
<string name="dialog_folder_info_device_list">Dela mapp med:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Något gick fel i Syncthing Lite. Du kan visa detaljerna från inställningarna för Syncthing Lite.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,49 @@
|
||||
<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="invalid_device_id">错误:无效的设备 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="loading_config_starting_syncthing_client">正在载入配置,启动 syncthing 客户端……</string>
|
||||
<string name="last_modified_time">最后修改:%1$s</string>
|
||||
<string name="remove_device_title">移除设备: %1$s?</string>
|
||||
<string name="remove_device_message">从已知设备列表中移除 %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>
|
||||
<string name="show_device_id">显示设备 ID</string>
|
||||
<string name="device_id">设备 ID</string>
|
||||
<string name="device_id_copied">设备 ID 已复制到剪贴板</string>
|
||||
<string name="share_device_id_chooser">分享设备 ID 于</string>
|
||||
<string name="other_syncthing_instance_title">另一个 Syncthing 实例正在运行</string>
|
||||
<string name="other_syncthing_instance_message">本地发现将无法工作。停止其他 Syncthing 实例以启用本地发现。</string>
|
||||
<string name="intro_page_one_title">欢迎使用 Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing 以开放、可靠并去中心化的软件替换掉封闭的云服务。您的数据仍由您拥有,您可以选择它们的存储位置,如果要共享给第三方您还可以选择如何在互联网上传输它们。</string>
|
||||
<string name="intro_page_two_title">添加设备</string>
|
||||
<string name="intro_page_three_title">共享您的文件夹</string>
|
||||
<string name="intro_page_two_description">输入 Syncthing 设备 ID,或者通过 QR 码扫描设备 ID</string>
|
||||
<string name="intro_page_three_description">已接受 ID 为%1$s 的设备,并与它共享了一个文件夹。设备连接可能需要花上数分钟。</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="settings_app_version_title">应用版本</string>
|
||||
<string name="settings_local_device_name">本地设备名称</string>
|
||||
<string name="settings_local_device_summary">此设备将被其他设备看到的名称</string>
|
||||
<string name="settings_shutdown_delay_title">关闭延迟</string>
|
||||
<string name="device_id_dialog_title">输入设备 ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 秒</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 秒</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 分钟</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 分钟</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s(%2$s)</string>
|
||||
</resources>
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#f43703</color>
|
||||
<color name="primary_dark">#d13602</color>
|
||||
<color name="primary_dark">#b90000</color>
|
||||
<color name="accent">#FFC107</color>
|
||||
<color name="divider">#1F000000</color>
|
||||
|
||||
<color name="intro_primary">#ff5252</color>
|
||||
<color name="intro_primary_dark">#c50e29</color>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user