mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-28 22:34:25 +02:00
Workouts: Add function to set header photo
This commit is contained in:
@@ -79,7 +79,7 @@ public class GBDaoGenerator {
|
||||
outputDir.mkdirs();
|
||||
}
|
||||
|
||||
final Schema schema = new Schema(135, MAIN_PACKAGE + ".entities");
|
||||
final Schema schema = new Schema(136, MAIN_PACKAGE + ".entities");
|
||||
|
||||
final List<Entity> sampleProvidersToGenerate = new LinkedList<>();
|
||||
|
||||
@@ -1642,6 +1642,7 @@ public class GBDaoGenerator {
|
||||
|
||||
summary.addStringProperty("gpxTrack").codeBeforeGetter(OVERRIDE);
|
||||
summary.addStringProperty("rawDetailsPath");
|
||||
summary.addStringProperty("headerPhoto");
|
||||
|
||||
Property deviceId = summary.addLongProperty("deviceId").notNull().codeBeforeGetter(OVERRIDE).getProperty();
|
||||
summary.addToOne(device, deviceId);
|
||||
|
||||
@@ -292,6 +292,7 @@ dependencies {
|
||||
implementation libs.androidx.lifecycle.process
|
||||
implementation libs.androidx.security.crypto
|
||||
implementation libs.androidx.browser
|
||||
implementation libs.androidx.exifinterface
|
||||
|
||||
implementation libs.material
|
||||
implementation libs.flexbox
|
||||
|
||||
+1
@@ -148,6 +148,7 @@ public class ActivityListingAdapter extends AbstractActivityListingAdapter<Activ
|
||||
session.getIntensity(),
|
||||
session.getEndTime().getTime() - session.getStartTime().getTime(),
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
position % 2 == 1,
|
||||
selected
|
||||
|
||||
+1
@@ -92,6 +92,7 @@ public class ActivityListingDetail extends DialogFragment {
|
||||
item.getIntensity(),
|
||||
item.getEndTime().getTime() - item.getStartTime().getTime(),
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false
|
||||
|
||||
+40
-1
@@ -20,6 +20,7 @@ import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
@@ -115,7 +116,7 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
workoutEditor = WorkoutEditor(requireContext())
|
||||
workoutEditor = WorkoutEditor(requireContext(), this)
|
||||
arguments?.let {
|
||||
workoutId = it.getLong(ARG_WORKOUT_ID, -1)
|
||||
}
|
||||
@@ -257,6 +258,14 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
)
|
||||
|
||||
view?.let {
|
||||
// Header photo
|
||||
if (summary.headerPhoto == null) {
|
||||
binding.headerphoto.setImageDrawable(null)
|
||||
} else {
|
||||
binding.headerphoto.setImageURI(Uri.fromFile(File(summary.headerPhoto)))
|
||||
}
|
||||
|
||||
// Activity icon
|
||||
binding.itemImage.setImageResource(
|
||||
ActivityKind.fromCode(summary.activityKind).icon
|
||||
)
|
||||
@@ -599,6 +608,32 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_summary_detail_action_add_photo -> {
|
||||
currentWorkout?.let {
|
||||
workoutEditor.setHeaderPhoto(it, object : WorkoutEditor.Callback {
|
||||
override fun onWorkoutUpdated() {
|
||||
notifyWorkoutChanged()
|
||||
// Reload only the workout header
|
||||
updateWorkoutHeader(it.summary)
|
||||
}
|
||||
})
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_summary_detail_action_remove_photo -> {
|
||||
currentWorkout?.let {
|
||||
workoutEditor.removeHeaderPhoto(it, object : WorkoutEditor.Callback {
|
||||
override fun onWorkoutUpdated() {
|
||||
notifyWorkoutChanged()
|
||||
// Reload only the workout header
|
||||
updateWorkoutHeader(it.summary)
|
||||
}
|
||||
})
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_summary_detail_action_edit_gps -> {
|
||||
currentWorkout?.let {
|
||||
workoutEditor.editGpsTrack(it, object : WorkoutEditor.Callback {
|
||||
@@ -649,6 +684,10 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
devToolsMenu?.isVisible = devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems()
|
||||
}
|
||||
|
||||
val overflowMenu2 = menu.findItem(R.id.activity_detail_overflowMenu2)?.subMenu
|
||||
overflowMenu2?.findItem(R.id.activity_summary_detail_action_add_photo)?.isVisible = workout.summary.headerPhoto == null
|
||||
overflowMenu2?.findItem(R.id.activity_summary_detail_action_remove_photo)?.isVisible = workout.summary.headerPhoto != null
|
||||
|
||||
// Endurain accepts FIT (built from the summary alone if needed), so it is offered
|
||||
// for any workout. Wanderer only supports GPX uploads, so it requires a GPS track.
|
||||
val endurainVm: EndurainSetupViewModel by viewModels()
|
||||
|
||||
+234
-1
@@ -1,16 +1,45 @@
|
||||
/* Copyright (C) 2020-2026 José Rebelo, Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge 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.
|
||||
|
||||
Gadgetbridge 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 nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.text.InputType
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
@@ -18,13 +47,20 @@ import java.io.IOException
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class WorkoutEditor(private val context: Context) {
|
||||
class WorkoutEditor(private val context: Context, resultCaller: ActivityResultCaller) {
|
||||
var filesGpxList: MutableList<String> = ArrayList()
|
||||
var selectedGpxIndex: Int = 0
|
||||
var selectedGpxFile: String? = null
|
||||
var exportPathRoot: File? = null // original gpx file location, could still contain old gpx files
|
||||
var exportPathGpx: File? = null // new gpx file location
|
||||
|
||||
private val pickHeaderPhotoLauncher: ActivityResultLauncher<PickVisualMediaRequest> =
|
||||
resultCaller.registerForActivityResult(
|
||||
ActivityResultContracts.PickVisualMedia()
|
||||
) { uri: Uri? -> onHeaderPhotoPicked(uri) }
|
||||
private var pendingHeaderPhotoWorkout: Workout? = null
|
||||
private var pendingHeaderPhotoCallback: Callback? = null
|
||||
|
||||
fun editWorkoutName(workout: Workout, callback: Callback) {
|
||||
val input = EditText(context).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
@@ -139,11 +175,208 @@ class WorkoutEditor(private val context: Context) {
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun setHeaderPhoto(workout: Workout, callback: Callback) {
|
||||
pendingHeaderPhotoWorkout = workout
|
||||
pendingHeaderPhotoCallback = callback
|
||||
pickHeaderPhotoLauncher.launch(
|
||||
PickVisualMediaRequest.Builder()
|
||||
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun removeHeaderPhoto(workout: Workout, callback: Callback) {
|
||||
val photoFile = File(workout.summary.headerPhoto)
|
||||
if (photoFile.exists())
|
||||
photoFile.delete()
|
||||
workout.summary.headerPhoto = null
|
||||
workout.summary.update()
|
||||
callback.onWorkoutUpdated()
|
||||
}
|
||||
|
||||
private fun onHeaderPhotoPicked(uri: Uri?) {
|
||||
val workout = pendingHeaderPhotoWorkout
|
||||
val callback = pendingHeaderPhotoCallback
|
||||
pendingHeaderPhotoWorkout = null
|
||||
pendingHeaderPhotoCallback = null
|
||||
|
||||
if (uri == null || workout == null || callback == null) {
|
||||
// user backed out of the picker, nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
val savedPath = copyHeaderPhotoToAppStorage(uri)
|
||||
if (savedPath != null) {
|
||||
workout.summary.headerPhoto = savedPath
|
||||
workout.summary.update()
|
||||
callback.onWorkoutUpdated()
|
||||
} else {
|
||||
GB.toast(
|
||||
context,
|
||||
"Importing photo failed",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyHeaderPhotoToAppStorage(uri: Uri): String? {
|
||||
return try {
|
||||
val exportPathPhotos = File(getPath(), "workout_photos")
|
||||
if (!exportPathPhotos.exists() && !exportPathPhotos.mkdirs()) {
|
||||
LOG.error("Failed to create photos directory: {}", exportPathPhotos)
|
||||
return null
|
||||
}
|
||||
|
||||
val mimeType = context.contentResolver.getType(uri)
|
||||
val extension = mimeType
|
||||
?.let { mime -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) }
|
||||
?: "jpg"
|
||||
val destFile = File(exportPathPhotos, "workout_header_${System.currentTimeMillis()}.$extension")
|
||||
LOG.debug("Photo destination path: {}", destFile)
|
||||
|
||||
// Read photo dimensions
|
||||
val boundsStream = context.contentResolver.openInputStream(uri) ?: return null
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
boundsStream.use { input -> BitmapFactory.decodeStream(input, null, bounds) }
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
|
||||
LOG.debug("Photo not decodable")
|
||||
return null
|
||||
}
|
||||
|
||||
// Determine rotation
|
||||
val orientation = readExifOrientation(uri)
|
||||
val orientationSwapsDimensions = orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
|
||||
orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
|
||||
orientation == ExifInterface.ORIENTATION_TRANSPOSE ||
|
||||
orientation == ExifInterface.ORIENTATION_TRANSVERSE
|
||||
val needsRotation = orientation != ExifInterface.ORIENTATION_NORMAL &&
|
||||
orientation != ExifInterface.ORIENTATION_UNDEFINED
|
||||
LOG.debug("Photo needs rotation: {}", needsRotation)
|
||||
|
||||
// Determine whether resize is needed
|
||||
val effectiveWidth = if (orientationSwapsDimensions) bounds.outHeight else bounds.outWidth
|
||||
val needsResize = effectiveWidth > MAX_HEADER_PHOTO_WIDTH_PX
|
||||
LOG.debug("Photo needs resize: {}", needsResize)
|
||||
|
||||
if (!needsRotation && !needsResize) {
|
||||
val copied = context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
destFile.outputStream().use { output -> input.copyTo(output) }
|
||||
true
|
||||
} ?: false
|
||||
if (!copied) {
|
||||
LOG.debug("Photo could not be copied")
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
val processed = decodeRotateAndResize(uri, bounds.outWidth, orientation, needsResize) ?: return null
|
||||
val compressFormat = when {
|
||||
mimeType?.contains("png") == true -> Bitmap.CompressFormat.PNG
|
||||
else -> Bitmap.CompressFormat.JPEG
|
||||
}
|
||||
val written = destFile.outputStream().use { output ->
|
||||
processed.compress(compressFormat, 90, output)
|
||||
}
|
||||
processed.recycle()
|
||||
if (!written) {
|
||||
LOG.debug("Photo could not be rotated and resized")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
destFile.path
|
||||
} catch (e: IOException) {
|
||||
LOG.error("Error copying header photo", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readExifOrientation(uri: Uri): Int {
|
||||
return try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
ExifInterface(input).getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_UNDEFINED
|
||||
)
|
||||
} ?: ExifInterface.ORIENTATION_UNDEFINED
|
||||
} catch (_: IOException) {
|
||||
// If no EXIF info found
|
||||
ExifInterface.ORIENTATION_UNDEFINED
|
||||
}
|
||||
}
|
||||
|
||||
private fun matrixForExifOrientation(orientation: Int): Matrix {
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
|
||||
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||
matrix.postRotate(90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
||||
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||
matrix.postRotate(-90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(-90f)
|
||||
else -> {} // ORIENTATION_NORMAL / ORIENTATION_UNDEFINED
|
||||
}
|
||||
return matrix
|
||||
}
|
||||
|
||||
private fun decodeRotateAndResize(
|
||||
uri: Uri,
|
||||
originalWidth: Int,
|
||||
orientation: Int,
|
||||
needsResize: Boolean
|
||||
): Bitmap? {
|
||||
val sampleSize = if (needsResize) calculateInSampleSize(originalWidth, MAX_HEADER_PHOTO_WIDTH_PX) else 1
|
||||
val decodeOptions = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
||||
val sampledBitmap = context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
BitmapFactory.decodeStream(input, null, decodeOptions)
|
||||
} ?: return null
|
||||
|
||||
val orientationMatrix = matrixForExifOrientation(orientation)
|
||||
val rotatedBitmap = if (orientationMatrix.isIdentity) {
|
||||
sampledBitmap
|
||||
} else {
|
||||
Bitmap.createBitmap(
|
||||
sampledBitmap, 0, 0, sampledBitmap.width, sampledBitmap.height, orientationMatrix, true
|
||||
).also {
|
||||
if (it !== sampledBitmap) sampledBitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
if (rotatedBitmap.width <= MAX_HEADER_PHOTO_WIDTH_PX) {
|
||||
return rotatedBitmap
|
||||
}
|
||||
|
||||
val scaledHeight =
|
||||
(rotatedBitmap.height.toFloat() / rotatedBitmap.width * MAX_HEADER_PHOTO_WIDTH_PX).toInt()
|
||||
val resized = rotatedBitmap.scale(MAX_HEADER_PHOTO_WIDTH_PX, scaledHeight)
|
||||
if (resized !== rotatedBitmap) {
|
||||
rotatedBitmap.recycle()
|
||||
}
|
||||
return resized
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(originalWidth: Int, targetWidth: Int): Int {
|
||||
var sampleSize = 1
|
||||
var halfWidth = originalWidth / 2
|
||||
while (halfWidth / sampleSize >= targetWidth) {
|
||||
sampleSize *= 2
|
||||
}
|
||||
return sampleSize
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onWorkoutUpdated()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(WorkoutEditor::class.java)
|
||||
private const val MAX_HEADER_PHOTO_WIDTH_PX = 1000
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -105,6 +105,7 @@ class WorkoutSummariesAdapter(
|
||||
-1f,
|
||||
summary.endTime.time - summary.startTime.time,
|
||||
hasGps,
|
||||
workout.summary.headerPhoto != null,
|
||||
summary.startTime,
|
||||
position % 2 == 1,
|
||||
selected
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/* Copyright (C) 2026 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge 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.
|
||||
|
||||
Gadgetbridge 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 nodomain.freeyourgadget.gadgetbridge.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
|
||||
public class GadgetbridgeUpdate_136 implements DBUpdateScript {
|
||||
@Override
|
||||
public void upgradeSchema(final SQLiteDatabase db) {
|
||||
if (!DBHelper.existsColumn(BaseActivitySummaryDao.TABLENAME, BaseActivitySummaryDao.Properties.HeaderPhoto.columnName, db)) {
|
||||
final String statement = "ALTER TABLE " + BaseActivitySummaryDao.TABLENAME + " ADD COLUMN \""
|
||||
+ BaseActivitySummaryDao.Properties.HeaderPhoto.columnName + "\" TEXT";
|
||||
db.execSQL(statement);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downgradeSchema(final SQLiteDatabase db) {
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ public class ActivityListItem {
|
||||
private final RelativeLayout parentLayout;
|
||||
private final ImageView activityIcon;
|
||||
private final ImageView gpsIcon;
|
||||
private final ImageView photoIcon;
|
||||
|
||||
private final int backgroundColor;
|
||||
private final int alternateColor;
|
||||
@@ -71,6 +72,7 @@ public class ActivityListItem {
|
||||
|
||||
this.activityIcon = itemView.findViewById(R.id.line_layout_activity_icon);
|
||||
this.gpsIcon = itemView.findViewById(R.id.line_layout_gps_icon);
|
||||
this.photoIcon = itemView.findViewById(R.id.line_layout_photo_icon);
|
||||
|
||||
this.backgroundColor = 0;
|
||||
this.alternateColor = getThemedColor(itemView.getContext(), R.attr.alternate_row_background);
|
||||
@@ -87,6 +89,7 @@ public class ActivityListItem {
|
||||
final float intensity,
|
||||
final long duration,
|
||||
final boolean hasGps,
|
||||
final boolean hasHeaderPhoto,
|
||||
@Nullable final Date date,
|
||||
final boolean zebraStripe,
|
||||
final boolean selected) {
|
||||
@@ -150,6 +153,12 @@ public class ActivityListItem {
|
||||
gpsIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (hasHeaderPhoto) {
|
||||
photoIcon.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
photoIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
activityIcon.setImageResource(activityKind.getIcon());
|
||||
|
||||
if (parentLayout != null) {
|
||||
|
||||
@@ -137,6 +137,13 @@
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/line_layout_photo_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_height="19dp"
|
||||
android:contentDescription="@string/candidate_item_device_image"
|
||||
app:srcCompat="@android:drawable/ic_menu_camera" />
|
||||
<ImageView
|
||||
android:id="@+id/line_layout_gps_icon"
|
||||
android:layout_width="20dp"
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- header -->
|
||||
<ImageView
|
||||
android:id="@+id/headerphoto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
android:title="@string/activity_summary_edit_name_title"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/activity_summary_detail_action_add_photo"
|
||||
android:icon="@drawable/ic_camera_remote"
|
||||
android:title="@string/activity_summary_set_header_photo_title"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/activity_summary_detail_action_remove_photo"
|
||||
android:icon="@drawable/ic_camera_remote"
|
||||
android:title="@string/activity_summary_remove_header_photo_title"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/activity_summary_detail_action_edit_gps"
|
||||
android:icon="@drawable/ic_gps_edit"
|
||||
|
||||
@@ -5195,4 +5195,6 @@
|
||||
<string name="qs_tile_label_8">Gadgetbridge 8</string>
|
||||
<string name="qs_tile_label_9">Gadgetbridge 9</string>
|
||||
<string name="qs_tile_label_10">Gadgetbridge 10</string>
|
||||
<string name="activity_summary_set_header_photo_title">Set header photo</string>
|
||||
<string name="activity_summary_remove_header_photo_title">Remove header photo</string>
|
||||
</resources>
|
||||
|
||||
@@ -52,6 +52,7 @@ slf4j = "2.0.18"
|
||||
solarpositioning = "0.1.10"
|
||||
security-crypto = "1.1.0"
|
||||
browser = "1.10.0"
|
||||
exifinterface = "1.3.6"
|
||||
|
||||
[libraries]
|
||||
ahocorasick = { group = "org.ahocorasick", name = "ahocorasick", version.ref = "ahocorasick" }
|
||||
@@ -116,6 +117,7 @@ slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||
solarpositioning = { group = "net.e175.klaus", name = "solarpositioning", version.ref = "solarpositioning" }
|
||||
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security-crypto" }
|
||||
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
|
||||
androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "exifinterface" }
|
||||
|
||||
[bundles]
|
||||
android-emojify = [
|
||||
|
||||
Reference in New Issue
Block a user