mirror of
https://github.com/nextcloud/android.git
synced 2024-11-24 14:15:44 +03:00
- overhaul metadata capture and processing
- exception handling in NominatimClient - set map boundaries - format strings according to locale - overlay drawables for photo pin correctly - refactored ImageDetailFragment - add copyright notices - add support for metadata from server - tint icons for dark mode Signed-off-by: tobiasKaminsky <tobias@kaminsky.me> Signed-off-by: ZetaTom <70907959+ZetaTom@users.noreply.github.com>
This commit is contained in:
parent
a88480051a
commit
f57be44343
31 changed files with 2095 additions and 93 deletions
|
@ -318,6 +318,8 @@ dependencies {
|
||||||
// dependencies for image cropping and rotation
|
// dependencies for image cropping and rotation
|
||||||
implementation 'com.vanniktech:android-image-cropper:4.5.0'
|
implementation 'com.vanniktech:android-image-cropper:4.5.0'
|
||||||
|
|
||||||
|
implementation 'org.osmdroid:osmdroid-android:6.1.16'
|
||||||
|
|
||||||
implementation('org.mnode.ical4j:ical4j:3.0.0') {
|
implementation('org.mnode.ical4j:ical4j:3.0.0') {
|
||||||
['org.apache.commons', 'commons-logging'].each {
|
['org.apache.commons', 'commons-logging'].each {
|
||||||
exclude group: "$it"
|
exclude group: "$it"
|
||||||
|
|
1149
app/schemas/com.nextcloud.client.database.NextcloudDatabase/72.json
Normal file
1149
app/schemas/com.nextcloud.client.database.NextcloudDatabase/72.json
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
BIN
app/src/androidTest/assets/gps.jpg
Normal file
BIN
app/src/androidTest/assets/gps.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
|
@ -32,6 +32,8 @@ import com.owncloud.android.db.OCUpload;
|
||||||
import com.owncloud.android.files.services.FileUploader;
|
import com.owncloud.android.files.services.FileUploader;
|
||||||
import com.owncloud.android.files.services.NameCollisionPolicy;
|
import com.owncloud.android.files.services.NameCollisionPolicy;
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.GeoLocation;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.ImageDimension;
|
||||||
import com.owncloud.android.operations.RefreshFolderOperation;
|
import com.owncloud.android.operations.RefreshFolderOperation;
|
||||||
import com.owncloud.android.operations.RemoveFileOperation;
|
import com.owncloud.android.operations.RemoveFileOperation;
|
||||||
import com.owncloud.android.operations.UploadFileOperation;
|
import com.owncloud.android.operations.UploadFileOperation;
|
||||||
|
@ -52,6 +54,7 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import static junit.framework.TestCase.assertEquals;
|
import static junit.framework.TestCase.assertEquals;
|
||||||
import static junit.framework.TestCase.assertFalse;
|
import static junit.framework.TestCase.assertFalse;
|
||||||
|
import static junit.framework.TestCase.assertNotNull;
|
||||||
import static junit.framework.TestCase.assertTrue;
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -469,7 +472,59 @@ public class UploadIT extends AbstractOnServerIT {
|
||||||
assertEquals(remotePath, ocFile.getRemotePath());
|
assertEquals(remotePath, ocFile.getRemotePath());
|
||||||
assertEquals(creationTimestamp, ocFile.getCreationTimestamp());
|
assertEquals(creationTimestamp, ocFile.getCreationTimestamp());
|
||||||
assertTrue(uploadTimestamp - 10 < ocFile.getUploadTimestamp() ||
|
assertTrue(uploadTimestamp - 10 < ocFile.getUploadTimestamp() ||
|
||||||
uploadTimestamp + 10 > ocFile.getUploadTimestamp());
|
uploadTimestamp + 10 > ocFile.getUploadTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMetadata() throws IOException {
|
||||||
|
File file = getFile("gps.jpg");
|
||||||
|
String remotePath = "/gps.jpg";
|
||||||
|
OCUpload ocUpload = new OCUpload(file.getAbsolutePath(), remotePath, account.name);
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
new UploadFileOperation(
|
||||||
|
uploadsStorageManager,
|
||||||
|
connectivityServiceMock,
|
||||||
|
powerManagementServiceMock,
|
||||||
|
user,
|
||||||
|
null,
|
||||||
|
ocUpload,
|
||||||
|
NameCollisionPolicy.DEFAULT,
|
||||||
|
FileUploader.LOCAL_BEHAVIOUR_COPY,
|
||||||
|
targetContext,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
getStorageManager()
|
||||||
|
)
|
||||||
|
.setRemoteFolderToBeCreated()
|
||||||
|
.execute(client)
|
||||||
|
.isSuccess()
|
||||||
|
);
|
||||||
|
|
||||||
|
// RefreshFolderOperation
|
||||||
|
assertTrue(new RefreshFolderOperation(getStorageManager().getFileByDecryptedRemotePath("/"),
|
||||||
|
System.currentTimeMillis() / 1000,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
getStorageManager(),
|
||||||
|
user,
|
||||||
|
targetContext).execute(client).isSuccess());
|
||||||
|
|
||||||
|
List<OCFile> files = getStorageManager().getFolderContent(getStorageManager().getFileByDecryptedRemotePath("/"),
|
||||||
|
false);
|
||||||
|
|
||||||
|
OCFile ocFile = null;
|
||||||
|
for (OCFile f : files) {
|
||||||
|
if (f.getFileName().equals("gps.jpg")) {
|
||||||
|
ocFile = f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(ocFile);
|
||||||
|
assertEquals(remotePath, ocFile.getRemotePath());
|
||||||
|
assertEquals(new ImageDimension(451f, 529f), ocFile.getImageDimension());
|
||||||
|
assertEquals(new GeoLocation(49.99679166666667, 8.67198611111111), ocFile.getGeoLocation());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyStoragePath(OCFile file) {
|
private void verifyStoragePath(OCFile file) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ package com.owncloud.android.ui.fragment
|
||||||
|
|
||||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||||
import com.nextcloud.test.TestActivity
|
import com.nextcloud.test.TestActivity
|
||||||
|
import com.nextcloud.ui.ImageDetailFragment
|
||||||
import com.owncloud.android.AbstractIT
|
import com.owncloud.android.AbstractIT
|
||||||
import com.owncloud.android.R
|
import com.owncloud.android.R
|
||||||
import com.owncloud.android.datamodel.OCFile
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
@ -40,13 +41,16 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
|
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
|
||||||
|
|
||||||
val file = OCFile("/")
|
var file = getFile("gps.jpg")
|
||||||
|
val oCFile = OCFile("/").apply {
|
||||||
|
storagePath = file.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ScreenshotTest
|
@ScreenshotTest
|
||||||
fun showFileDetailActivitiesFragment() {
|
fun showFileDetailActivitiesFragment() {
|
||||||
val sut = testActivityRule.launchActivity(null)
|
val sut = testActivityRule.launchActivity(null)
|
||||||
sut.addFragment(FileDetailActivitiesFragment.newInstance(file, user))
|
sut.addFragment(FileDetailActivitiesFragment.newInstance(oCFile, user))
|
||||||
|
|
||||||
waitForIdleSync()
|
waitForIdleSync()
|
||||||
shortSleep()
|
shortSleep()
|
||||||
|
@ -58,7 +62,19 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
|
||||||
@ScreenshotTest
|
@ScreenshotTest
|
||||||
fun showFileDetailSharingFragment() {
|
fun showFileDetailSharingFragment() {
|
||||||
val sut = testActivityRule.launchActivity(null)
|
val sut = testActivityRule.launchActivity(null)
|
||||||
sut.addFragment(FileDetailSharingFragment.newInstance(file, user))
|
sut.addFragment(FileDetailSharingFragment.newInstance(oCFile, user))
|
||||||
|
|
||||||
|
waitForIdleSync()
|
||||||
|
shortSleep()
|
||||||
|
shortSleep()
|
||||||
|
screenshot(sut)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ScreenshotTest
|
||||||
|
fun showFileDetailDetailsFragment() {
|
||||||
|
val sut = testActivityRule.launchActivity(null)
|
||||||
|
sut.addFragment(ImageDetailFragment.newInstance(oCFile, user))
|
||||||
|
|
||||||
waitForIdleSync()
|
waitForIdleSync()
|
||||||
shortSleep()
|
shortSleep()
|
||||||
|
@ -71,7 +87,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
fun showDetailsActivities() {
|
fun showDetailsActivities() {
|
||||||
val activity = testActivityRule.launchActivity(null)
|
val activity = testActivityRule.launchActivity(null)
|
||||||
val sut = FileDetailFragment.newInstance(file, user, 0)
|
val sut = FileDetailFragment.newInstance(oCFile, user, 0)
|
||||||
activity.addFragment(sut)
|
activity.addFragment(sut)
|
||||||
|
|
||||||
waitForIdleSync()
|
waitForIdleSync()
|
||||||
|
@ -141,7 +157,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
|
||||||
// @ScreenshotTest
|
// @ScreenshotTest
|
||||||
fun showDetailsActivitiesNone() {
|
fun showDetailsActivitiesNone() {
|
||||||
val activity = testActivityRule.launchActivity(null)
|
val activity = testActivityRule.launchActivity(null)
|
||||||
val sut = FileDetailFragment.newInstance(file, user, 0)
|
val sut = FileDetailFragment.newInstance(oCFile, user, 0)
|
||||||
activity.addFragment(sut)
|
activity.addFragment(sut)
|
||||||
|
|
||||||
waitForIdleSync()
|
waitForIdleSync()
|
||||||
|
@ -159,7 +175,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
|
||||||
@ScreenshotTest
|
@ScreenshotTest
|
||||||
fun showDetailsActivitiesError() {
|
fun showDetailsActivitiesError() {
|
||||||
val activity = testActivityRule.launchActivity(null)
|
val activity = testActivityRule.launchActivity(null)
|
||||||
val sut = FileDetailFragment.newInstance(file, user, 0)
|
val sut = FileDetailFragment.newInstance(oCFile, user, 0)
|
||||||
activity.addFragment(sut)
|
activity.addFragment(sut)
|
||||||
|
|
||||||
waitForIdleSync()
|
waitForIdleSync()
|
||||||
|
@ -179,7 +195,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
|
||||||
@ScreenshotTest
|
@ScreenshotTest
|
||||||
fun showDetailsSharing() {
|
fun showDetailsSharing() {
|
||||||
val sut = testActivityRule.launchActivity(null)
|
val sut = testActivityRule.launchActivity(null)
|
||||||
sut.addFragment(FileDetailFragment.newInstance(file, user, 1))
|
sut.addFragment(FileDetailFragment.newInstance(oCFile, user, 1))
|
||||||
|
|
||||||
waitForIdleSync()
|
waitForIdleSync()
|
||||||
|
|
||||||
|
|
|
@ -29,12 +29,12 @@ import android.graphics.Paint
|
||||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||||
import com.nextcloud.test.TestActivity
|
import com.nextcloud.test.TestActivity
|
||||||
import com.owncloud.android.AbstractIT
|
import com.owncloud.android.AbstractIT
|
||||||
import com.owncloud.android.datamodel.ImageDimension
|
|
||||||
import com.owncloud.android.datamodel.OCFile
|
import com.owncloud.android.datamodel.OCFile
|
||||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager.InitDiskCacheTask
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager.InitDiskCacheTask
|
||||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC
|
import com.owncloud.android.lib.common.utils.Log_OC
|
||||||
|
import com.owncloud.android.lib.resources.files.model.ImageDimension
|
||||||
import com.owncloud.android.utils.ScreenshotTest
|
import com.owncloud.android.utils.ScreenshotTest
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
|
|
96
app/src/main/java/com/nextcloud/client/NominatimClient.kt
Normal file
96
app/src/main/java/com/nextcloud/client/NominatimClient.kt
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud Android client application
|
||||||
|
*
|
||||||
|
* @author ZetaTom
|
||||||
|
* Copyright (C) 2023 ZetaTom
|
||||||
|
* Copyright (C) 2023 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.nextcloud.client
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.owncloud.android.MainApp
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.internal.http.HTTP_OK
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class NominatimClient constructor(geocoderBaseUrl: String, email: String) {
|
||||||
|
private val client = OkHttpClient()
|
||||||
|
private val gson = Gson()
|
||||||
|
private val reverseUrl = "${geocoderBaseUrl}reverse?format=jsonv2&email=${URLEncoder.encode(email, ENCODING_UTF_8)}"
|
||||||
|
|
||||||
|
private fun doRequest(requestUrl: String): String? {
|
||||||
|
val request = Request.Builder().url(requestUrl).header(HEADER_USER_AGENT, MainApp.getUserAgent()).build()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
if (response.code == HTTP_OK) {
|
||||||
|
return response.body.string()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse geocode specified location - get human readable name suitable for displaying from given coordinates.
|
||||||
|
*
|
||||||
|
* @param latitude GPS latitude
|
||||||
|
* @param longitude GPS longitude
|
||||||
|
* @param zoom level of detail to request
|
||||||
|
*/
|
||||||
|
fun reverseGeocode(
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
zoom: ZoomLevel = ZoomLevel.TOWN_BOROUGH
|
||||||
|
): ReverseGeocodingResult? {
|
||||||
|
val response = doRequest("$reverseUrl&addressdetails=0&zoom=${zoom.int}&lat=$latitude&lon=$longitude")
|
||||||
|
return response?.let { gson.fromJson(it, ReverseGeocodingResult::class.java) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ENCODING_UTF_8 = "UTF-8"
|
||||||
|
private const val HEADER_USER_AGENT = "User-Agent"
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
enum class ZoomLevel(val int: Int) {
|
||||||
|
COUNTRY(3),
|
||||||
|
STATE(5),
|
||||||
|
COUNTY(8),
|
||||||
|
CITY(10),
|
||||||
|
TOWN_BOROUGH(12),
|
||||||
|
VILLAGE_SUBURB(13),
|
||||||
|
NEIGHBOURHOOD(14),
|
||||||
|
LOCALITY(15),
|
||||||
|
MAJOR_STREETS(16),
|
||||||
|
MINOR_STREETS(17),
|
||||||
|
BUILDING(18),
|
||||||
|
MAX(19)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReverseGeocodingResult(
|
||||||
|
@SerializedName("lat")
|
||||||
|
val latitude: Double,
|
||||||
|
@SerializedName("lon")
|
||||||
|
val longitude: Double,
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("display_name")
|
||||||
|
val displayName: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,7 +63,8 @@ import com.owncloud.android.db.ProviderMeta
|
||||||
AutoMigration(from = 65, to = 66),
|
AutoMigration(from = 65, to = 66),
|
||||||
AutoMigration(from = 66, to = 67),
|
AutoMigration(from = 66, to = 67),
|
||||||
AutoMigration(from = 68, to = 69),
|
AutoMigration(from = 68, to = 69),
|
||||||
AutoMigration(from = 69, to = 70)
|
AutoMigration(from = 69, to = 70),
|
||||||
|
AutoMigration(from = 71, to = 72)
|
||||||
],
|
],
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
|
|
|
@ -119,5 +119,7 @@ data class FileEntity(
|
||||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TOKEN)
|
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TOKEN)
|
||||||
val lockToken: String?,
|
val lockToken: String?,
|
||||||
@ColumnInfo(name = ProviderTableMeta.FILE_TAGS)
|
@ColumnInfo(name = ProviderTableMeta.FILE_TAGS)
|
||||||
val tags: String?
|
val tags: String?,
|
||||||
|
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
|
||||||
|
val metadataGPS: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -50,6 +50,7 @@ class LegacyMigration(
|
||||||
*
|
*
|
||||||
* This is needed because the [Migration] does not know which versions it's dealing with
|
* This is needed because the [Migration] does not know which versions it's dealing with
|
||||||
*/
|
*/
|
||||||
|
@Suppress("ForEachOnRange")
|
||||||
fun RoomDatabase.Builder<NextcloudDatabase>.addLegacyMigrations(
|
fun RoomDatabase.Builder<NextcloudDatabase>.addLegacyMigrations(
|
||||||
clock: Clock
|
clock: Clock
|
||||||
): RoomDatabase.Builder<NextcloudDatabase> {
|
): RoomDatabase.Builder<NextcloudDatabase> {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
|
||||||
import com.nextcloud.client.widget.DashboardWidgetProvider;
|
import com.nextcloud.client.widget.DashboardWidgetProvider;
|
||||||
import com.nextcloud.client.widget.DashboardWidgetService;
|
import com.nextcloud.client.widget.DashboardWidgetService;
|
||||||
import com.nextcloud.ui.ChooseAccountDialogFragment;
|
import com.nextcloud.ui.ChooseAccountDialogFragment;
|
||||||
|
import com.nextcloud.ui.ImageDetailFragment;
|
||||||
import com.nextcloud.ui.SetStatusDialogFragment;
|
import com.nextcloud.ui.SetStatusDialogFragment;
|
||||||
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
|
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
|
||||||
import com.nmc.android.ui.LauncherActivity;
|
import com.nmc.android.ui.LauncherActivity;
|
||||||
|
@ -475,4 +476,6 @@ abstract class ComponentsModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract EditImageActivity editImageActivity();
|
abstract EditImageActivity editImageActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ImageDetailFragment imageDetailFragment();
|
||||||
}
|
}
|
||||||
|
|
411
app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt
Normal file
411
app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt
Normal file
|
@ -0,0 +1,411 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud Android client application
|
||||||
|
*
|
||||||
|
* @author ZetaTom
|
||||||
|
* Copyright (C) 2023 ZetaTom
|
||||||
|
* Copyright (C) 2023 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.nextcloud.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||||
|
import com.nextcloud.client.NominatimClient
|
||||||
|
import com.nextcloud.client.account.User
|
||||||
|
import com.nextcloud.client.di.Injectable
|
||||||
|
import com.owncloud.android.MainApp
|
||||||
|
import com.owncloud.android.R
|
||||||
|
import com.owncloud.android.databinding.PreviewImageDetailsFragmentBinding
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||||
|
import com.owncloud.android.utils.BitmapUtils
|
||||||
|
import com.owncloud.android.utils.DisplayUtils
|
||||||
|
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.osmdroid.config.Configuration
|
||||||
|
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||||
|
import org.osmdroid.util.GeoPoint
|
||||||
|
import org.osmdroid.views.CustomZoomButtonsController
|
||||||
|
import org.osmdroid.views.overlay.ItemizedIconOverlay
|
||||||
|
import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener
|
||||||
|
import org.osmdroid.views.overlay.OverlayItem
|
||||||
|
import java.lang.Long.max
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class ImageDetailFragment : Fragment(), Injectable {
|
||||||
|
private lateinit var binding: PreviewImageDetailsFragmentBinding
|
||||||
|
private lateinit var file: OCFile
|
||||||
|
private lateinit var user: User
|
||||||
|
private lateinit var metadata: ImageMetadata
|
||||||
|
private lateinit var nominatimClient: NominatimClient
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewThemeUtils: ViewThemeUtils
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = PreviewImageDetailsFragmentBinding.inflate(layoutInflater, container, false)
|
||||||
|
|
||||||
|
binding.fileDetailsIcon.setImageDrawable(
|
||||||
|
viewThemeUtils.platform.tintDrawable(
|
||||||
|
requireContext(),
|
||||||
|
R.drawable.outline_image_24,
|
||||||
|
ColorRole.ON_BACKGROUND
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.cameraInformationIcon.setImageDrawable(
|
||||||
|
viewThemeUtils.platform.tintDrawable(
|
||||||
|
requireContext(),
|
||||||
|
R.drawable.outline_camera_24,
|
||||||
|
ColorRole.ON_BACKGROUND
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val arguments = arguments ?: throw IllegalStateException("arguments are mandatory")
|
||||||
|
file = arguments.getParcelable(ARG_FILE)!!
|
||||||
|
user = arguments.getParcelable(ARG_USER)!!
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
file = savedInstanceState.getParcelable(ARG_FILE)!!
|
||||||
|
user = savedInstanceState.getParcelable(ARG_USER)!!
|
||||||
|
metadata = savedInstanceState.getParcelable(ARG_METADATA)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
nominatimClient = NominatimClient(
|
||||||
|
getString(R.string.osm_geocoder_url), getString(R.string.osm_geocoder_contact)
|
||||||
|
)
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putParcelable(ARG_FILE, file)
|
||||||
|
outState.putParcelable(ARG_USER, user)
|
||||||
|
outState.putParcelable(ARG_METADATA, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
gatherMetadata()
|
||||||
|
setupFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LongMethod")
|
||||||
|
private fun setupFragment() {
|
||||||
|
binding.fileInformationTime.text = metadata.date
|
||||||
|
|
||||||
|
// detailed file information
|
||||||
|
val fileInformation = mutableListOf<String>()
|
||||||
|
if (metadata.length != null && metadata.width != null && metadata.length!! > 0 && metadata.width!! > 0) {
|
||||||
|
try {
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val pxlCount = when (val res = metadata.length!! * metadata.width!!.toLong()) {
|
||||||
|
in 0..999999 -> "%.2f".format(res / 1000000f)
|
||||||
|
in 1000000..9999999 -> "%.1f".format(res / 1000000f)
|
||||||
|
else -> (res / 1000000).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInformation.add(String.format(getString(R.string.image_preview_unit_megapixel), pxlCount))
|
||||||
|
fileInformation.add("${metadata.width!!} × ${metadata.length!!}")
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata.fileSize?.let { fileInformation.add(it) }
|
||||||
|
|
||||||
|
if (fileInformation.isNotEmpty()) {
|
||||||
|
binding.fileInformationDetails.text = fileInformation.joinToString(separator = TEXT_SEP)
|
||||||
|
binding.fileInformation.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageTakenConditions()
|
||||||
|
|
||||||
|
// initialise map and address views
|
||||||
|
metadata.location?.let { location ->
|
||||||
|
initMap(location.first, location.second)
|
||||||
|
binding.imageLocation.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
// launch reverse geocoding request
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val geocodingResult = nominatimClient.reverseGeocode(location.first, location.second)
|
||||||
|
if (geocodingResult != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binding.imageLocationText.visibility = View.VISIBLE
|
||||||
|
binding.imageLocationText.text = geocodingResult.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setImageTakenConditions() {
|
||||||
|
// camera make and model
|
||||||
|
val makeModel = if (metadata.make?.let { metadata.model?.contains(it) } == false) {
|
||||||
|
"${metadata.make} ${metadata.model}"
|
||||||
|
} else {
|
||||||
|
metadata.model ?: metadata.make
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.make == null || metadata.model?.contains(metadata.make!!) == true) {
|
||||||
|
binding.imgTCMakeModel.text = metadata.model
|
||||||
|
} else {
|
||||||
|
binding.imgTCMakeModel.text = "${metadata.make} ${metadata.model}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// image taking conditions
|
||||||
|
val imageTakingConditions = mutableListOf<String>()
|
||||||
|
metadata.aperture?.let {
|
||||||
|
imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_fnumber), it))
|
||||||
|
}
|
||||||
|
metadata.exposure?.let {
|
||||||
|
imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_seconds), it))
|
||||||
|
}
|
||||||
|
metadata.focalLen?.let {
|
||||||
|
imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_millimetres), it))
|
||||||
|
}
|
||||||
|
metadata.iso?.let {
|
||||||
|
imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_iso), it))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageTakingConditions.isNotEmpty() && makeModel != null) {
|
||||||
|
binding.imgTCMakeModel.text = makeModel
|
||||||
|
binding.imgTCConditions.text = imageTakingConditions.joinToString(separator = TEXT_SEP)
|
||||||
|
binding.imgTC.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun initMap(latitude: Double, longitude: Double, zoom: Double = 13.0) {
|
||||||
|
// required for OpenStreetMap
|
||||||
|
Configuration.getInstance().userAgentValue = MainApp.getUserAgent()
|
||||||
|
|
||||||
|
val location = GeoPoint(latitude, longitude)
|
||||||
|
|
||||||
|
binding.imageLocationMap.apply {
|
||||||
|
setTileSource(TileSourceFactory.MAPNIK)
|
||||||
|
|
||||||
|
// set expected boundaries
|
||||||
|
setScrollableAreaLimitLatitude(SCROLL_LIMIT, -SCROLL_LIMIT, 0)
|
||||||
|
isVerticalMapRepetitionEnabled = false
|
||||||
|
minZoomLevel = 2.0
|
||||||
|
maxZoomLevel = NominatimClient.Companion.ZoomLevel.MAX.int.toDouble()
|
||||||
|
|
||||||
|
// initial location
|
||||||
|
controller.setCenter(location)
|
||||||
|
controller.setZoom(zoom)
|
||||||
|
|
||||||
|
// scale labels to be legible
|
||||||
|
isTilesScaledToDpi = true
|
||||||
|
setZoomRounding(true)
|
||||||
|
|
||||||
|
// hide zoom buttons
|
||||||
|
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
||||||
|
|
||||||
|
// enable multi-touch zoom
|
||||||
|
setMultiTouchControls(true)
|
||||||
|
setOnTouchListener { v, _ ->
|
||||||
|
v.parent.requestDisallowInterceptTouchEvent(true)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
val markerOverlay = ItemizedIconOverlay(
|
||||||
|
mutableListOf(OverlayItem("Location", "", location)),
|
||||||
|
imagePinDrawable(context),
|
||||||
|
markerOnGestureListener(latitude, longitude),
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
overlays.add(markerOverlay)
|
||||||
|
|
||||||
|
onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add copyright notice
|
||||||
|
binding.imageLocationMapCopyright.text = binding.imageLocationMap.tileProvider.tileSource.copyrightNotice
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
private fun gatherMetadata() {
|
||||||
|
val fileSize = DisplayUtils.bytesToHumanReadable(file.fileLength)
|
||||||
|
var timestamp = max(file.modificationTimestamp, file.creationTimestamp)
|
||||||
|
if (file.isDown) {
|
||||||
|
val exif = androidx.exifinterface.media.ExifInterface(file.storagePath)
|
||||||
|
var length = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_IMAGE_LENGTH)?.toInt()
|
||||||
|
var width = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_IMAGE_WIDTH)?.toInt()
|
||||||
|
var exposure = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_SHUTTER_SPEED_VALUE)
|
||||||
|
|
||||||
|
// get timestamp from date string
|
||||||
|
exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_DATETIME)?.let {
|
||||||
|
timestamp = SimpleDateFormat("y:M:d H:m:s", Locale.ROOT).parse(it)?.time ?: timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// format exposure string
|
||||||
|
if (exposure == null) {
|
||||||
|
exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_TIME)?.let {
|
||||||
|
exposure = "1/" + (1 / it.toDouble()).toInt()
|
||||||
|
}
|
||||||
|
} else if ("/" in exposure!!) {
|
||||||
|
try {
|
||||||
|
exposure!!.split("/").also {
|
||||||
|
exposure = "1/" + 2f.pow(it[0].toFloat() / it[1].toFloat()).roundToInt()
|
||||||
|
}
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine size if not contained in exif data
|
||||||
|
if (width == null || length == null || width <= 0 || length <= 0) {
|
||||||
|
val res = BitmapUtils.getImageResolution(file.storagePath)
|
||||||
|
width = res[0]
|
||||||
|
length = res[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = ImageMetadata(
|
||||||
|
fileSize = fileSize,
|
||||||
|
length = length,
|
||||||
|
width = width,
|
||||||
|
exposure = exposure,
|
||||||
|
date = formatDate(timestamp),
|
||||||
|
location = exif.latLong?.let { Pair(it[0], it[1]) },
|
||||||
|
aperture = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_F_NUMBER),
|
||||||
|
focalLen = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM),
|
||||||
|
make = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_MAKE),
|
||||||
|
model = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_MODEL),
|
||||||
|
iso = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_ISO_SPEED) ?: exif.getAttribute(
|
||||||
|
androidx.exifinterface.media.ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// get metadata from server
|
||||||
|
val location = if (file.geoLocation == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Pair(file.geoLocation!!.latitude, file.geoLocation!!.longitude)
|
||||||
|
}
|
||||||
|
metadata = ImageMetadata(
|
||||||
|
fileSize = fileSize,
|
||||||
|
date = formatDate(timestamp),
|
||||||
|
location = location,
|
||||||
|
width = file.imageDimension?.width?.toInt(),
|
||||||
|
length = file.imageDimension?.height?.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
private fun formatDate(timestamp: Long): String {
|
||||||
|
return buildString {
|
||||||
|
append(SimpleDateFormat("EEEE").format(timestamp))
|
||||||
|
append(TEXT_SEP)
|
||||||
|
append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(timestamp))
|
||||||
|
append(TEXT_SEP)
|
||||||
|
append(DateFormat.getTimeInstance(DateFormat.SHORT).format(timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private fun imagePinDrawable(context: Context): LayerDrawable {
|
||||||
|
val bitmap =
|
||||||
|
ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId)
|
||||||
|
val foreground = BitmapUtils.bitmapToCircularBitmapDrawable(resources, bitmap)
|
||||||
|
val background = ContextCompat.getDrawable(context, R.drawable.photo_pin)
|
||||||
|
|
||||||
|
val layerDrawable = if (foreground != null) {
|
||||||
|
LayerDrawable(arrayOf(background, foreground))
|
||||||
|
} else {
|
||||||
|
val d = ContextCompat.getDrawable(context, R.drawable.file_image)
|
||||||
|
LayerDrawable(arrayOf(background, d))
|
||||||
|
}
|
||||||
|
|
||||||
|
val dp = DisplayUtils.convertDpToPixel(2f, context)
|
||||||
|
layerDrawable.apply {
|
||||||
|
setLayerSize(1, 38 * dp, 38 * dp)
|
||||||
|
setLayerSize(0, 40 * dp, 47 * dp)
|
||||||
|
setLayerInsetTop(1, dp)
|
||||||
|
setLayerGravity(1, Gravity.CENTER_HORIZONTAL)
|
||||||
|
}
|
||||||
|
return layerDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnItemGestureListener for marker in MapView.
|
||||||
|
*/
|
||||||
|
private fun markerOnGestureListener(latitude: Double, longitude: Double) =
|
||||||
|
object : OnItemGestureListener<OverlayItem> {
|
||||||
|
override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=$latitude,$longitude"))
|
||||||
|
DisplayUtils.startIntentIfAppAvailable(intent, activity, R.string.no_map_app_availble)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
private data class ImageMetadata(
|
||||||
|
val fileSize: String? = null,
|
||||||
|
val date: String? = null,
|
||||||
|
val length: Int? = null,
|
||||||
|
val width: Int? = null,
|
||||||
|
val exposure: String? = null,
|
||||||
|
val aperture: String? = null,
|
||||||
|
val focalLen: String? = null,
|
||||||
|
val iso: String? = null,
|
||||||
|
val make: String? = null,
|
||||||
|
val model: String? = null,
|
||||||
|
val location: Pair<Double, Double>? = null
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_FILE = "FILE"
|
||||||
|
private const val ARG_USER = "USER"
|
||||||
|
private const val ARG_METADATA = "METADATA"
|
||||||
|
private const val TEXT_SEP = " • "
|
||||||
|
private const val SCROLL_LIMIT = 80.0
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(file: OCFile, user: User): ImageDetailFragment {
|
||||||
|
return ImageDetailFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(ARG_FILE, file)
|
||||||
|
putParcelable(ARG_USER, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
|
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
|
||||||
import com.owncloud.android.lib.resources.files.model.FileLockType;
|
import com.owncloud.android.lib.resources.files.model.FileLockType;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.GeoLocation;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.ImageDimension;
|
||||||
import com.owncloud.android.lib.resources.files.model.RemoteFile;
|
import com.owncloud.android.lib.resources.files.model.RemoteFile;
|
||||||
import com.owncloud.android.lib.resources.shares.OCShare;
|
import com.owncloud.android.lib.resources.shares.OCShare;
|
||||||
import com.owncloud.android.lib.resources.shares.ShareType;
|
import com.owncloud.android.lib.resources.shares.ShareType;
|
||||||
|
@ -440,7 +442,6 @@ public class FileDataStorageManager {
|
||||||
*/
|
*/
|
||||||
private ContentValues createContentValuesBase(OCFile fileOrFolder) {
|
private ContentValues createContentValuesBase(OCFile fileOrFolder) {
|
||||||
final ContentValues cv = new ContentValues();
|
final ContentValues cv = new ContentValues();
|
||||||
final Gson gson = new Gson();
|
|
||||||
cv.put(ProviderTableMeta.FILE_MODIFIED, fileOrFolder.getModificationTimestamp());
|
cv.put(ProviderTableMeta.FILE_MODIFIED, fileOrFolder.getModificationTimestamp());
|
||||||
cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, fileOrFolder.getModificationTimestampAtLastSyncForData());
|
cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, fileOrFolder.getModificationTimestampAtLastSyncForData());
|
||||||
cv.put(ProviderTableMeta.FILE_PARENT, fileOrFolder.getParentId());
|
cv.put(ProviderTableMeta.FILE_PARENT, fileOrFolder.getParentId());
|
||||||
|
@ -507,7 +508,8 @@ public class FileDataStorageManager {
|
||||||
cv.put(ProviderTableMeta.FILE_LOCK_TIMEOUT, file.getLockTimeout());
|
cv.put(ProviderTableMeta.FILE_LOCK_TIMEOUT, file.getLockTimeout());
|
||||||
cv.put(ProviderTableMeta.FILE_LOCK_TOKEN, file.getLockToken());
|
cv.put(ProviderTableMeta.FILE_LOCK_TOKEN, file.getLockToken());
|
||||||
cv.put(ProviderTableMeta.FILE_MODIFIED, file.getModificationTimestamp());
|
cv.put(ProviderTableMeta.FILE_MODIFIED, file.getModificationTimestamp());
|
||||||
cv.put(ProviderTableMeta.FILE_METADATA_SIZE, new Gson().toJson(file.getImageDimension()));
|
cv.put(ProviderTableMeta.FILE_METADATA_SIZE, gson.toJson(file.getImageDimension()));
|
||||||
|
cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation()));
|
||||||
|
|
||||||
return cv;
|
return cv;
|
||||||
}
|
}
|
||||||
|
@ -978,6 +980,16 @@ public class FileDataStorageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String metadataGPS = fileEntity.getMetadataGPS();
|
||||||
|
// Surprisingly JSON deserialization causes significant overhead.
|
||||||
|
// Avoid it in common, trivial cases (null/empty).
|
||||||
|
if (!(metadataGPS == null || metadataGPS.isEmpty() || JSON_NULL_STRING.equals(metadataGPS))) {
|
||||||
|
GeoLocation geoLocation = gson.fromJson(metadataGPS, GeoLocation.class);
|
||||||
|
if (geoLocation != null) {
|
||||||
|
ocFile.setGeoLocation(geoLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ocFile;
|
return ocFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* Nextcloud Android client application
|
|
||||||
*
|
|
||||||
* @author Tobias Kaminsky
|
|
||||||
* Copyright (C) 2022 Tobias Kaminsky
|
|
||||||
* Copyright (C) 2022 Nextcloud GmbH
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.owncloud.android.datamodel
|
|
||||||
|
|
||||||
data class ImageDimension(var width: Float = -1f, var height: Float = -1f)
|
|
|
@ -34,6 +34,8 @@ import com.owncloud.android.lib.common.network.WebdavEntry;
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
import com.owncloud.android.lib.common.network.WebdavUtils;
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
import com.owncloud.android.lib.resources.files.model.FileLockType;
|
import com.owncloud.android.lib.resources.files.model.FileLockType;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.GeoLocation;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.ImageDimension;
|
||||||
import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
|
import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
|
||||||
import com.owncloud.android.lib.resources.shares.ShareeUser;
|
import com.owncloud.android.lib.resources.shares.ShareeUser;
|
||||||
import com.owncloud.android.utils.MimeType;
|
import com.owncloud.android.utils.MimeType;
|
||||||
|
@ -115,11 +117,13 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
|
||||||
private String lockToken;
|
private String lockToken;
|
||||||
@Nullable
|
@Nullable
|
||||||
private ImageDimension imageDimension;
|
private ImageDimension imageDimension;
|
||||||
|
@Nullable
|
||||||
|
private GeoLocation geolocation;
|
||||||
private List<String> tags = new ArrayList<>();
|
private List<String> tags = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URI to the local path of the file contents, if stored in the device; cached after first call to {@link
|
* URI to the local path of the file contents, if stored in the device; cached after first call to
|
||||||
* #getStorageUri()}
|
* {@link #getStorageUri()}
|
||||||
*/
|
*/
|
||||||
private Uri localUri;
|
private Uri localUri;
|
||||||
|
|
||||||
|
@ -975,6 +979,15 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
|
||||||
return imageDimension;
|
return imageDimension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setGeoLocation(@Nullable GeoLocation geolocation) {
|
||||||
|
this.geolocation = geolocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public GeoLocation getGeoLocation() {
|
||||||
|
return geolocation;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ import com.owncloud.android.lib.common.OwnCloudClient;
|
||||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
|
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
|
import com.owncloud.android.lib.resources.files.model.ImageDimension;
|
||||||
import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
|
import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
|
||||||
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
|
||||||
import com.owncloud.android.ui.TextDrawable;
|
import com.owncloud.android.ui.TextDrawable;
|
||||||
|
|
|
@ -35,7 +35,7 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class ProviderMeta {
|
public class ProviderMeta {
|
||||||
public static final String DB_NAME = "filelist";
|
public static final String DB_NAME = "filelist";
|
||||||
public static final int DB_VERSION = 71;
|
public static final int DB_VERSION = 72;
|
||||||
|
|
||||||
private ProviderMeta() {
|
private ProviderMeta() {
|
||||||
// No instance
|
// No instance
|
||||||
|
@ -117,6 +117,7 @@ public class ProviderMeta {
|
||||||
public static final String FILE_SHAREES = "sharees";
|
public static final String FILE_SHAREES = "sharees";
|
||||||
public static final String FILE_RICH_WORKSPACE = "rich_workspace";
|
public static final String FILE_RICH_WORKSPACE = "rich_workspace";
|
||||||
public static final String FILE_METADATA_SIZE = "metadata_size";
|
public static final String FILE_METADATA_SIZE = "metadata_size";
|
||||||
|
public static final String FILE_METADATA_GPS = "metadata_gps";
|
||||||
public static final String FILE_LOCKED = "locked";
|
public static final String FILE_LOCKED = "locked";
|
||||||
public static final String FILE_LOCK_TYPE = "lock_type";
|
public static final String FILE_LOCK_TYPE = "lock_type";
|
||||||
public static final String FILE_LOCK_OWNER = "lock_owner";
|
public static final String FILE_LOCK_OWNER = "lock_owner";
|
||||||
|
@ -128,52 +129,53 @@ public class ProviderMeta {
|
||||||
public static final String FILE_TAGS = "tags";
|
public static final String FILE_TAGS = "tags";
|
||||||
|
|
||||||
public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
|
public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
|
||||||
_ID,
|
_ID,
|
||||||
FILE_PARENT,
|
FILE_PARENT,
|
||||||
FILE_NAME,
|
FILE_NAME,
|
||||||
FILE_ENCRYPTED_NAME,
|
FILE_ENCRYPTED_NAME,
|
||||||
FILE_CREATION,
|
FILE_CREATION,
|
||||||
FILE_MODIFIED,
|
FILE_MODIFIED,
|
||||||
FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
|
FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
|
||||||
FILE_CONTENT_LENGTH,
|
FILE_CONTENT_LENGTH,
|
||||||
FILE_CONTENT_TYPE,
|
FILE_CONTENT_TYPE,
|
||||||
FILE_STORAGE_PATH,
|
FILE_STORAGE_PATH,
|
||||||
FILE_PATH,
|
FILE_PATH,
|
||||||
FILE_PATH_DECRYPTED,
|
FILE_PATH_DECRYPTED,
|
||||||
FILE_ACCOUNT_OWNER,
|
FILE_ACCOUNT_OWNER,
|
||||||
FILE_LAST_SYNC_DATE,
|
FILE_LAST_SYNC_DATE,
|
||||||
FILE_LAST_SYNC_DATE_FOR_DATA,
|
FILE_LAST_SYNC_DATE_FOR_DATA,
|
||||||
FILE_KEEP_IN_SYNC,
|
FILE_KEEP_IN_SYNC,
|
||||||
FILE_ETAG,
|
FILE_ETAG,
|
||||||
FILE_ETAG_ON_SERVER,
|
FILE_ETAG_ON_SERVER,
|
||||||
FILE_SHARED_VIA_LINK,
|
FILE_SHARED_VIA_LINK,
|
||||||
FILE_SHARED_WITH_SHAREE,
|
FILE_SHARED_WITH_SHAREE,
|
||||||
FILE_PERMISSIONS,
|
FILE_PERMISSIONS,
|
||||||
FILE_REMOTE_ID,
|
FILE_REMOTE_ID,
|
||||||
FILE_LOCAL_ID,
|
FILE_LOCAL_ID,
|
||||||
FILE_UPDATE_THUMBNAIL,
|
FILE_UPDATE_THUMBNAIL,
|
||||||
FILE_IS_DOWNLOADING,
|
FILE_IS_DOWNLOADING,
|
||||||
FILE_ETAG_IN_CONFLICT,
|
FILE_ETAG_IN_CONFLICT,
|
||||||
FILE_FAVORITE,
|
FILE_FAVORITE,
|
||||||
FILE_IS_ENCRYPTED,
|
FILE_IS_ENCRYPTED,
|
||||||
FILE_MOUNT_TYPE,
|
FILE_MOUNT_TYPE,
|
||||||
FILE_HAS_PREVIEW,
|
FILE_HAS_PREVIEW,
|
||||||
FILE_UNREAD_COMMENTS_COUNT,
|
FILE_UNREAD_COMMENTS_COUNT,
|
||||||
FILE_OWNER_ID,
|
FILE_OWNER_ID,
|
||||||
FILE_OWNER_DISPLAY_NAME,
|
FILE_OWNER_DISPLAY_NAME,
|
||||||
FILE_NOTE,
|
FILE_NOTE,
|
||||||
FILE_SHAREES,
|
FILE_SHAREES,
|
||||||
FILE_RICH_WORKSPACE,
|
FILE_RICH_WORKSPACE,
|
||||||
FILE_LOCKED,
|
FILE_LOCKED,
|
||||||
FILE_LOCK_TYPE,
|
FILE_LOCK_TYPE,
|
||||||
FILE_LOCK_OWNER,
|
FILE_LOCK_OWNER,
|
||||||
FILE_LOCK_OWNER_DISPLAY_NAME,
|
FILE_LOCK_OWNER_DISPLAY_NAME,
|
||||||
FILE_LOCK_OWNER_EDITOR,
|
FILE_LOCK_OWNER_EDITOR,
|
||||||
FILE_LOCK_TIMESTAMP,
|
FILE_LOCK_TIMESTAMP,
|
||||||
FILE_LOCK_TIMEOUT,
|
FILE_LOCK_TIMEOUT,
|
||||||
FILE_LOCK_TOKEN,
|
FILE_LOCK_TOKEN,
|
||||||
FILE_METADATA_SIZE,
|
FILE_METADATA_SIZE,
|
||||||
FILE_TAGS));
|
FILE_TAGS,
|
||||||
|
FILE_METADATA_GPS));
|
||||||
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
|
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
|
||||||
|
|
||||||
// Columns of ocshares table
|
// Columns of ocshares table
|
||||||
|
|
|
@ -21,10 +21,12 @@
|
||||||
package com.owncloud.android.ui.adapter;
|
package com.owncloud.android.ui.adapter;
|
||||||
|
|
||||||
import com.nextcloud.client.account.User;
|
import com.nextcloud.client.account.User;
|
||||||
|
import com.nextcloud.ui.ImageDetailFragment;
|
||||||
import com.owncloud.android.datamodel.OCFile;
|
import com.owncloud.android.datamodel.OCFile;
|
||||||
import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment;
|
import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment;
|
||||||
import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
|
import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
|
||||||
import com.owncloud.android.utils.EncryptionUtils;
|
import com.owncloud.android.utils.EncryptionUtils;
|
||||||
|
import com.owncloud.android.utils.MimeTypeUtil;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
@ -40,6 +42,7 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
|
||||||
|
|
||||||
private FileDetailSharingFragment fileDetailSharingFragment;
|
private FileDetailSharingFragment fileDetailSharingFragment;
|
||||||
private FileDetailActivitiesFragment fileDetailActivitiesFragment;
|
private FileDetailActivitiesFragment fileDetailActivitiesFragment;
|
||||||
|
private ImageDetailFragment imageDetailFragment;
|
||||||
|
|
||||||
public FileDetailTabAdapter(FragmentManager fm, OCFile file, User user) {
|
public FileDetailTabAdapter(FragmentManager fm, OCFile file, User user) {
|
||||||
super(fm);
|
super(fm);
|
||||||
|
@ -58,6 +61,9 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
|
||||||
case 1:
|
case 1:
|
||||||
fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, user);
|
fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, user);
|
||||||
return fileDetailSharingFragment;
|
return fileDetailSharingFragment;
|
||||||
|
case 2:
|
||||||
|
imageDetailFragment = ImageDetailFragment.newInstance(file, user);
|
||||||
|
return imageDetailFragment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,18 +75,23 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
|
||||||
return fileDetailActivitiesFragment;
|
return fileDetailActivitiesFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImageDetailFragment getImageDetailFragment() {
|
||||||
|
return imageDetailFragment;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
if (file.isEncrypted()) {
|
if (file.isEncrypted()) {
|
||||||
if (EncryptionUtils.supportsSecureFiledrop(file, user)) {
|
if (EncryptionUtils.supportsSecureFiledrop(file, user)) {
|
||||||
return 2;
|
return 2;
|
||||||
} else {
|
|
||||||
// sharing not allowed for encrypted files, thus only show first tab (activities)
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
} else {
|
// sharing not allowed for encrypted files, thus only show first tab (activities)
|
||||||
// unencrypted files/folders
|
return 1;
|
||||||
return 2;
|
|
||||||
}
|
}
|
||||||
|
// unencrypted files/folders
|
||||||
|
if (MimeTypeUtil.isImage(file)) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,9 +32,9 @@ import com.owncloud.android.R
|
||||||
import com.owncloud.android.databinding.GalleryRowBinding
|
import com.owncloud.android.databinding.GalleryRowBinding
|
||||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||||
import com.owncloud.android.datamodel.GalleryRow
|
import com.owncloud.android.datamodel.GalleryRow
|
||||||
import com.owncloud.android.datamodel.ImageDimension
|
|
||||||
import com.owncloud.android.datamodel.OCFile
|
import com.owncloud.android.datamodel.OCFile
|
||||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||||
|
import com.owncloud.android.lib.resources.files.model.ImageDimension
|
||||||
import com.owncloud.android.utils.BitmapUtils
|
import com.owncloud.android.utils.BitmapUtils
|
||||||
import com.owncloud.android.utils.DisplayUtils
|
import com.owncloud.android.utils.DisplayUtils
|
||||||
|
|
||||||
|
|
|
@ -304,6 +304,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
|
||||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.shared_via_users));
|
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.shared_via_users));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (MimeTypeUtil.isImage(getFile())) {
|
||||||
|
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.image_32dp));
|
||||||
|
}
|
||||||
|
|
||||||
viewThemeUtils.material.themeTabLayout(binding.tabLayout);
|
viewThemeUtils.material.themeTabLayout(binding.tabLayout);
|
||||||
|
|
||||||
final FileDetailTabAdapter adapter = new FileDetailTabAdapter(getFragmentManager(), getFile(), user);
|
final FileDetailTabAdapter adapter = new FileDetailTabAdapter(getFragmentManager(), getFile(), user);
|
||||||
|
@ -543,6 +547,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
|
||||||
// remains there
|
// remains there
|
||||||
setButtonsForRemote();
|
setButtonsForRemote();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FloatingActionButton fabMain = requireActivity().findViewById(R.id.fab_main);
|
||||||
|
if (fabMain != null) {
|
||||||
|
fabMain.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupViewPager();
|
setupViewPager();
|
||||||
|
|
|
@ -205,6 +205,13 @@ public final class BitmapUtils {
|
||||||
return resultBitmap;
|
return resultBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int[] getImageResolution(String srcPath) {
|
||||||
|
Options options = new Options();
|
||||||
|
options.inJustDecodeBounds = true;
|
||||||
|
BitmapFactory.decodeFile(srcPath, options);
|
||||||
|
return new int [] {options.outWidth, options.outHeight};
|
||||||
|
}
|
||||||
|
|
||||||
public static Color usernameToColor(String name) {
|
public static Color usernameToColor(String name) {
|
||||||
String hash = name.toLowerCase(Locale.ROOT);
|
String hash = name.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
@ -346,6 +353,7 @@ public final class BitmapUtils {
|
||||||
* @param bitmap the original bitmap
|
* @param bitmap the original bitmap
|
||||||
* @return the circular bitmap
|
* @return the circular bitmap
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources,
|
public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources,
|
||||||
Bitmap bitmap,
|
Bitmap bitmap,
|
||||||
float radius) {
|
float radius) {
|
||||||
|
@ -363,6 +371,7 @@ public final class BitmapUtils {
|
||||||
return roundedBitmap;
|
return roundedBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources, Bitmap bitmap) {
|
public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources, Bitmap bitmap) {
|
||||||
return bitmapToCircularBitmapDrawable(resources, bitmap, -1);
|
return bitmapToCircularBitmapDrawable(resources, bitmap, -1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,6 +245,8 @@ public final class FileStorageUtils {
|
||||||
file.setLockTimeout(remote.getLockTimeout());
|
file.setLockTimeout(remote.getLockTimeout());
|
||||||
file.setLockToken(remote.getLockToken());
|
file.setLockToken(remote.getLockToken());
|
||||||
file.setTags(new ArrayList<>(Arrays.asList(remote.getTags())));
|
file.setTags(new ArrayList<>(Arrays.asList(remote.getTags())));
|
||||||
|
file.setImageDimension(remote.getImageDimension());
|
||||||
|
file.setGeoLocation(remote.getGeoLocation());
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
5
app/src/main/res/drawable/outline_camera_24.xml
Normal file
5
app/src/main/res/drawable/outline_camera_24.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:autoMirrored="true" android:height="24dp"
|
||||||
|
android:tint="#000000" android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M14.25,2.26l-0.08,-0.04 -0.01,0.02C13.46,2.09 12.74,2 12,2 6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10c0,-4.75 -3.31,-8.72 -7.75,-9.74zM19.41,9h-7.99l2.71,-4.7c2.4,0.66 4.35,2.42 5.28,4.7zM13.1,4.08L10.27,9l-1.15,2L6.4,6.3C7.84,4.88 9.82,4 12,4c0.37,0 0.74,0.03 1.1,0.08zM5.7,7.09L8.54,12l1.15,2L4.26,14C4.1,13.36 4,12.69 4,12c0,-1.85 0.64,-3.55 1.7,-4.91zM4.59,15h7.98l-2.71,4.7c-2.4,-0.67 -4.34,-2.42 -5.27,-4.7zM10.9,19.91L14.89,13l2.72,4.7C16.16,19.12 14.18,20 12,20c-0.38,0 -0.74,-0.04 -1.1,-0.09zM18.3,16.91l-4,-6.91h5.43c0.17,0.64 0.27,1.31 0.27,2 0,1.85 -0.64,3.55 -1.7,4.91z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/outline_image_24.xml
Normal file
5
app/src/main/res/drawable/outline_image_24.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:autoMirrored="true" android:height="24dp"
|
||||||
|
android:tint="#000000" android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"/>
|
||||||
|
</vector>
|
36
app/src/main/res/drawable/photo_pin.xml
Normal file
36
app/src/main/res/drawable/photo_pin.xml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<!--
|
||||||
|
Nextcloud Android client application
|
||||||
|
|
||||||
|
@author ZetaTom
|
||||||
|
Copyright (C) 2023 ZetaTom
|
||||||
|
Copyright (C) 2023 Nextcloud GmbH
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="46.61dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="46.61">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/grey_600"
|
||||||
|
android:pathData="M1.523,20a18.477,18.477 0,1 0,36.954 0a18.477,18.477 0,1 0,-36.954 0z"
|
||||||
|
android:strokeWidth="3.0454"
|
||||||
|
android:strokeColor="@color/grey_600" />
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/grey_600"
|
||||||
|
android:pathData="m1.5,20c0,17.795 18.5,25 18.5,25s18.5,-7.205 18.5,-25"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:strokeColor="@color/grey_600" />
|
||||||
|
</vector>
|
171
app/src/main/res/layout/preview_image_details_fragment.xml
Normal file
171
app/src/main/res/layout/preview_image_details_fragment.xml
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Nextcloud Android client application
|
||||||
|
|
||||||
|
@author ZetaTom
|
||||||
|
Copyright (C) 2023 ZetaTom
|
||||||
|
Copyright (C) 2023 Nextcloud GmbH
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/fileInformation"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/file_details_icon"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:contentDescription="@string/image_preview_filedetails"
|
||||||
|
android:padding="6dp"
|
||||||
|
|
||||||
|
android:src="@drawable/outline_image_24" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fileInformation_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Wednesday • 26 Jul 2023 • 12:27" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fileInformation_details"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
tools:text="12 MP • 3024 × 4032 • 923 KB" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/imgTC"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/camera_information_icon"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:contentDescription="@string/image_preview_image_taking_conditions"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:src="@drawable/outline_camera_24" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/imgTC_makeModel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Camera Phone (4th generation)" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/imgTC_conditions"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
tools:text="ƒ/1.8 • 1/374 s • 28 mm • ISO 200" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/imageLocation"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="330dp"
|
||||||
|
android:background="@drawable/rounded_rect"
|
||||||
|
android:elevation="1dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/imageLocation_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="Mitte, Berlin, Germany"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.osmdroid.views.MapView
|
||||||
|
android:id="@+id/imageLocation_map"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/imageLocation_map_copyright"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:padding="5dp"
|
||||||
|
tools:text="© OpenStreetMap contributors" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
|
@ -121,6 +121,11 @@
|
||||||
<string name="splashScreenBold"></string>
|
<string name="splashScreenBold"></string>
|
||||||
<string name="splashScreenNormal"></string>
|
<string name="splashScreenNormal"></string>
|
||||||
|
|
||||||
|
<!-- Geocoding -->
|
||||||
|
<string name="osm_geocoder_url" translatable="false">https://nominatim.openstreetmap.org/</string>
|
||||||
|
<string name="osm_geocoder_contact" translatable="false">android@nextcloud.com</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Dev settings -->
|
<!-- Dev settings -->
|
||||||
<string name="dev_link">https://download.nextcloud.com/android/dev/nextcloud-dev-</string>
|
<string name="dev_link">https://download.nextcloud.com/android/dev/nextcloud-dev-</string>
|
||||||
<string name="dev_latest">https://download.nextcloud.com/android/dev/latest</string>
|
<string name="dev_latest">https://download.nextcloud.com/android/dev/latest</string>
|
||||||
|
|
|
@ -805,6 +805,7 @@
|
||||||
<string name="no_browser_available">No app available to handle links</string>
|
<string name="no_browser_available">No app available to handle links</string>
|
||||||
<string name="no_pdf_app_available">No App available to handle PDF</string>
|
<string name="no_pdf_app_available">No App available to handle PDF</string>
|
||||||
<string name="no_email_app_available">No App available to handle mail address</string>
|
<string name="no_email_app_available">No App available to handle mail address</string>
|
||||||
|
<string name="no_map_app_availble">No App available to handle maps</string>
|
||||||
<string name="share_via_link_hide_download">Hide download</string>
|
<string name="share_via_link_hide_download">Hide download</string>
|
||||||
<string name="unread_comments">Unread comments exist</string>
|
<string name="unread_comments">Unread comments exist</string>
|
||||||
<string name="richdocuments_failed_to_load_document">Failed to load document!</string>
|
<string name="richdocuments_failed_to_load_document">Failed to load document!</string>
|
||||||
|
@ -1098,4 +1099,12 @@
|
||||||
<string name="ecosystem_apps_display_notes">Notes</string>
|
<string name="ecosystem_apps_display_notes">Notes</string>
|
||||||
<string name="ecosystem_apps_display_talk">Talk</string>
|
<string name="ecosystem_apps_display_talk">Talk</string>
|
||||||
<string name="ecosystem_apps_display_more">More</string>
|
<string name="ecosystem_apps_display_more">More</string>
|
||||||
|
<string name="filedetails_details">Details</string>
|
||||||
|
<string name="image_preview_unit_millimetres">%s mm</string>
|
||||||
|
<string name="image_preview_unit_fnumber">ƒ/%s</string>
|
||||||
|
<string name="image_preview_unit_seconds">%s s</string>
|
||||||
|
<string name="image_preview_unit_iso">ISO %s</string>
|
||||||
|
<string name="image_preview_unit_megapixel">%s MP</string>
|
||||||
|
<string name="image_preview_filedetails">File details</string>
|
||||||
|
<string name="image_preview_image_taking_conditions">Image taking conditions</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue