Allow to share images when reading online. Move chapter cache to external cache dir. Dependency updates.
This commit is contained in:
parent
86b8712dd1
commit
d30c019b89
@ -38,7 +38,7 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 14
|
versionCode 15
|
||||||
versionName "0.3.2"
|
versionName "0.3.2"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
@ -112,12 +112,6 @@ dependencies {
|
|||||||
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
compile 'com.evernote:android-job:1.1.3'
|
|
||||||
compile 'com.google.android.gms:play-services-gcm:9.8.0'
|
|
||||||
|
|
||||||
compile 'com.github.seven332:unifile:0.2.0'
|
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.2.3'
|
compile 'io.reactivex:rxjava:1.2.3'
|
||||||
@ -145,12 +139,17 @@ dependencies {
|
|||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
compile 'com.squareup.duktape:duktape-android:1.1.0'
|
compile 'com.squareup.duktape:duktape-android:1.1.0'
|
||||||
|
|
||||||
// Disk cache
|
// Disk
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
compile 'com.github.seven332:unifile:1.0.0'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
compile 'org.jsoup:jsoup:1.10.1'
|
compile 'org.jsoup:jsoup:1.10.1'
|
||||||
|
|
||||||
|
// Job scheduling
|
||||||
|
compile 'com.evernote:android-job:1.1.3'
|
||||||
|
compile 'com.google.android.gms:play-services-gcm:10.0.0'
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.cache
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
@ -11,9 +11,9 @@ import eu.kanade.tachiyomi.util.saveTo
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Okio
|
import okio.Okio
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create chapter cache
|
* Class used to create chapter cache
|
||||||
@ -26,15 +26,6 @@ import java.lang.reflect.Type
|
|||||||
*/
|
*/
|
||||||
class ChapterCache(private val context: Context) {
|
class ChapterCache(private val context: Context) {
|
||||||
|
|
||||||
/** Google Json class used for parsing JSON files. */
|
|
||||||
private val gson: Gson = Gson()
|
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
|
||||||
private val diskCache: DiskLruCache
|
|
||||||
|
|
||||||
/** Page list collection used for deserializing from JSON. */
|
|
||||||
private val pageListCollection: Type = object : TypeToken<List<Page>>() {}.type
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Name of cache directory. */
|
/** Name of cache directory. */
|
||||||
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
||||||
@ -49,38 +40,37 @@ class ChapterCache(private val context: Context) {
|
|||||||
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
|
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
/** Google Json class used for parsing JSON files. */
|
||||||
// Open cache in default cache directory.
|
private val gson: Gson by injectLazy()
|
||||||
diskCache = DiskLruCache.open(
|
|
||||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
/** Cache class used for cache management. */
|
||||||
|
private val diskCache = DiskLruCache.open(
|
||||||
|
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_VALUE_COUNT,
|
||||||
PARAMETER_CACHE_SIZE)
|
PARAMETER_CACHE_SIZE)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns directory of cache.
|
* Returns directory of cache.
|
||||||
* @return directory of cache.
|
|
||||||
*/
|
*/
|
||||||
val cacheDir: File
|
val cacheDir: File
|
||||||
get() = diskCache.directory
|
get() = diskCache.directory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns real size of directory.
|
* Returns real size of directory.
|
||||||
* @return real size of directory.
|
|
||||||
*/
|
*/
|
||||||
private val realSize: Long
|
private val realSize: Long
|
||||||
get() = DiskUtil.getDirectorySize(cacheDir)
|
get() = DiskUtil.getDirectorySize(cacheDir)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns real size of directory in human readable format.
|
* Returns real size of directory in human readable format.
|
||||||
* @return real size of directory.
|
|
||||||
*/
|
*/
|
||||||
val readableSize: String
|
val readableSize: String
|
||||||
get() = Formatter.formatFileSize(context, realSize)
|
get() = Formatter.formatFileSize(context, realSize)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove file from cache.
|
* Remove file from cache.
|
||||||
|
*
|
||||||
* @param file name of file "md5.0".
|
* @param file name of file "md5.0".
|
||||||
* @return status of deletion for the file.
|
* @return status of deletion for the file.
|
||||||
*/
|
*/
|
||||||
@ -101,6 +91,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page list from cache.
|
* Get page list from cache.
|
||||||
|
*
|
||||||
* @param chapterUrl the url of the chapter.
|
* @param chapterUrl the url of the chapter.
|
||||||
* @return an observable of the list of pages.
|
* @return an observable of the list of pages.
|
||||||
*/
|
*/
|
||||||
@ -111,13 +102,14 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
diskCache.get(key).use {
|
diskCache.get(key).use {
|
||||||
gson.fromJson(it.getString(0), pageListCollection)
|
gson.fromJson<List<Page>>(it.getString(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add page list to disk cache.
|
* Add page list to disk cache.
|
||||||
|
*
|
||||||
* @param chapterUrl the url of the chapter.
|
* @param chapterUrl the url of the chapter.
|
||||||
* @param pages list of pages.
|
* @param pages list of pages.
|
||||||
*/
|
*/
|
||||||
@ -151,7 +143,8 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if image is in cache.
|
* Returns true if image is in cache.
|
||||||
|
*
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @return true if in cache otherwise false.
|
* @return true if in cache otherwise false.
|
||||||
*/
|
*/
|
||||||
@ -164,22 +157,20 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get image path from url.
|
* Get image file from url.
|
||||||
|
*
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @return path of image.
|
* @return path of image.
|
||||||
*/
|
*/
|
||||||
fun getImagePath(imageUrl: String): File? {
|
fun getImageFile(imageUrl: String): File {
|
||||||
try {
|
|
||||||
// Get file from md5 key.
|
// Get file from md5 key.
|
||||||
val imageName = DiskUtil.hashKeyForDisk(imageUrl) + ".0"
|
val imageName = DiskUtil.hashKeyForDisk(imageUrl) + ".0"
|
||||||
return File(diskCache.directory, imageName)
|
return File(diskCache.directory, imageName)
|
||||||
} catch (e: IOException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add image to cache.
|
* Add image to cache.
|
||||||
|
*
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @param response http response from page.
|
* @param response http response from page.
|
||||||
* @throws IOException image error.
|
* @throws IOException image error.
|
||||||
|
@ -41,8 +41,8 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
internal fun getMangaDir(source: Source, manga: Manga): UniFile {
|
internal fun getMangaDir(source: Source, manga: Manga): UniFile {
|
||||||
return downloadsDir
|
return downloadsDir
|
||||||
.subFile(getSourceDirName(source))!!
|
.createDirectory(getSourceDirName(source))
|
||||||
.subFile(getMangaDirName(manga))!!
|
.createDirectory(getMangaDirName(manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -247,7 +247,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
private fun downloadChapter(download: Download): Observable<Download> {
|
private fun downloadChapter(download: Download): Observable<Download> {
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val mangaDir = provider.getMangaDir(download.source, download.manga)
|
val mangaDir = provider.getMangaDir(download.source, download.manga)
|
||||||
val tmpDir = mangaDir.subFile("${chapterDirname}_tmp")!!
|
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
// Pull page list from network and add them to download object
|
// Pull page list from network and add them to download object
|
||||||
@ -262,8 +262,6 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
|
|
||||||
return pageListObservable
|
return pageListObservable
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
tmpDir.ensureDir()
|
|
||||||
|
|
||||||
// Delete all temporary (unfinished) files
|
// Delete all temporary (unfinished) files
|
||||||
tmpDir.listFiles()
|
tmpDir.listFiles()
|
||||||
?.filter { it.name!!.endsWith(".tmp") }
|
?.filter { it.name!!.endsWith(".tmp") }
|
||||||
|
@ -417,7 +417,7 @@ abstract class OnlineSource() : Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
page.uri = Uri.fromFile(chapterCache.getImagePath(imageUrl))
|
page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl))
|
||||||
page.status = Page.READY
|
page.status = Page.READY
|
||||||
}
|
}
|
||||||
.doOnError { page.status = Page.ERROR }
|
.doOnError { page.status = Page.ERROR }
|
||||||
|
@ -14,23 +14,29 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||||
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
|
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class ChangelogDialogFragment : DialogFragment() {
|
class ChangelogDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun show(preferences: PreferencesHelper, fragmentManager: FragmentManager) {
|
fun show(context: Context, preferences: PreferencesHelper, fm: FragmentManager) {
|
||||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
ChangelogDialogFragment().show(fragmentManager, "changelog")
|
ChangelogDialogFragment().show(fm, "changelog")
|
||||||
|
|
||||||
// FIXME Ugly check to restore jobs. Remove me in a few months :D
|
// TODO better upgrades management
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
|
// Restore jobs after upgrading to evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||||
UpdateCheckerJob.setupTask()
|
UpdateCheckerJob.setupTask()
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask()
|
LibraryUpdateJob.setupTask()
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 15) {
|
||||||
|
// Delete internal chapter cache dir.
|
||||||
|
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
||||||
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment
|
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadFragment
|
import eu.kanade.tachiyomi.ui.download.DownloadFragment
|
||||||
|
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
||||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
|
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
|
||||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
|
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
|
||||||
@ -79,7 +79,7 @@ class MainActivity : BaseActivity() {
|
|||||||
setSelectedDrawerItem(startScreenId)
|
setSelectedDrawerItem(startScreenId)
|
||||||
|
|
||||||
// Show changelog if needed
|
// Show changelog if needed
|
||||||
ChangelogDialogFragment.show(preferences, supportFragmentManager)
|
ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,7 +480,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
putExtra(Intent.EXTRA_STREAM, page.uri)
|
putExtra(Intent.EXTRA_STREAM, page.uri)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
}
|
}
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.os.Build
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -24,7 +23,6 @@ import rx.Subscription
|
|||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import rx.subjects.SerializedSubject
|
import rx.subjects.SerializedSubject
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||||
@ -216,12 +214,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = if (Build.VERSION.SDK_INT < 21 || UniFile.isFileUri(uri)) {
|
val file = UniFile.fromUri(context, uri)
|
||||||
UniFile.fromFile(File(uri.path))
|
|
||||||
} else {
|
|
||||||
// Tree uri returns the root folder
|
|
||||||
UniFile.fromSingleUri(context, uri)
|
|
||||||
}!!
|
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
page.status = Page.ERROR
|
page.status = Page.ERROR
|
||||||
return
|
return
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -20,7 +19,6 @@ import rx.Subscription
|
|||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import rx.subjects.SerializedSubject
|
import rx.subjects.SerializedSubject
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -250,12 +248,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = if (Build.VERSION.SDK_INT < 21 || UniFile.isFileUri(uri)) {
|
val file = UniFile.fromUri(context, uri)
|
||||||
UniFile.fromFile(File(uri.path))
|
|
||||||
} else {
|
|
||||||
// Tree uri returns the root folder
|
|
||||||
UniFile.fromSingleUri(context, uri)
|
|
||||||
}!!
|
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
page?.status = Page.ERROR
|
page?.status = Page.ERROR
|
||||||
return
|
return
|
||||||
|
@ -116,7 +116,7 @@ class SettingsDownloadsFragment : SettingsFragment() {
|
|||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
|
||||||
val file = UniFile.fromTreeUri(context, uri)
|
val file = UniFile.fromUri(context, uri)
|
||||||
preferences.downloadsDirectory().set(file.uri.toString())
|
preferences.downloadsDirectory().set(file.uri.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
<external-path name="pictures" path="Pictures" />
|
<external-path name="ext_files" path="." />
|
||||||
</paths>
|
</paths>
|
Loading…
Reference in New Issue
Block a user