1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2025-01-07 10:42:07 +00:00

Add "recent changes" UI to drawer (fixes #117) (#116)

* Add "recent changes" UI to drawer

* Update build.gradle - guava 26.0-android

* Add REST endpoint

* Add DiskEvent to model

* Add DiskEventData to model

* Add RestApi#getDiskEvents

* Add ChangeListAdapter#clear

* Implement data exchange between UI and service

* Display DiskEvents

* Add icons

* Return DiskEvents in reverse order

* Display device name instead of partial ID

* Format dateTime

* Update whatsnew

* Imported translations

* Update APK version to 0.14.51.12 / 4175

* Fix lint

* Review

* Update README.md
This commit is contained in:
Catfriend1 2018-10-28 22:46:51 +01:00 committed by GitHub
parent 8d59ba1a6c
commit 7ca62c946a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 491 additions and 5 deletions

View file

@ -2,10 +2,11 @@
[![License: MPLv2](https://img.shields.io/badge/License-MPLv2-blue.svg)](https://opensource.org/licenses/MPL-2.0)
<a href="https://github.com/Catfriend1/syncthing-android/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/Catfriend1/syncthing-android/all.svg" /></a>
<a href="https://f-droid.org/de/packages/com.github.catfriend1.syncthingandroid" alt="F-Droid release"><img src="https://img.shields.io/badge/f--droid-4170-brightgreen.svg" /></a>
<a href="https://f-droid.org/de/packages/com.github.catfriend1.syncthingandroid" alt="F-Droid release"><img src="https://img.shields.io/badge/f--droid-4173-brightgreen.svg" /></a>
# Major enhancements in this fork are:
- Individual sync conditions can be applied per device and per folder (for expert users).
- Recent changes UI.
- UI explains why syncthing is running or not running according to the run conditions set in preferences.
- "Battery eater" problem is fixed.
- Android 8 and 9 support.

View file

@ -10,7 +10,7 @@ dependencies {
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'org.mindrot:jbcrypt:0.4'
// com.google.guava:guava:24.1-jre will crash on Android 5.x
implementation 'com.google.guava:guava:23.6-android'
implementation 'com.google.guava:guava:26.0-android'
implementation 'com.annimon:stream:1.1.9'
implementation 'com.android.volley:volley:1.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
@ -37,8 +37,8 @@ android {
applicationId "com.github.catfriend1.syncthingandroid"
minSdkVersion 16
targetSdkVersion 26
versionCode 4174
versionName "0.14.51.11"
versionCode 4175
versionName "0.14.51.12"
testApplicationId 'com.github.catfriend1.syncthingandroid.test'
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
playAccountConfig = playAccountConfigs.defaultAccountConfig

View file

@ -46,6 +46,15 @@
android:label="@string/app_name"
android:launchMode="singleTask">
</activity>
<activity
android:name=".activities.RecentChangesActivity"
android:label="@string/recent_changes_title"
android:parentActivityName=".activities.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.MainActivity" />
</activity>
<activity
android:name=".activities.TipsAndTricksActivity"
android:label="@string/tips_and_tricks_title"

View file

@ -0,0 +1,139 @@
package com.nutomic.syncthingandroid.activities;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.model.Device;
import com.nutomic.syncthingandroid.model.DiskEvent;
import com.nutomic.syncthingandroid.service.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.service.SyncthingServiceBinder;
import com.nutomic.syncthingandroid.views.ChangeListAdapter;
import com.nutomic.syncthingandroid.views.ChangeListAdapter.ItemClickListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Holds a RecyclerView that shows recent changes to files and folders.
*/
public class RecentChangesActivity extends SyncthingActivity
implements SyncthingService.OnServiceStateChangeListener {
private static final String TAG = "RecentChangesActivity";
private static int DISK_EVENT_LIMIT = 100;
private List<Device> mDevices;
private ChangeListAdapter mRecentChangeAdapter;
private RecyclerView mRecyclerView;
private RecyclerView.LayoutManager mLayoutManager;
private SyncthingService.State mServiceState = SyncthingService.State.INIT;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recent_changes);
mRecyclerView = findViewById(R.id.changes_recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecentChangeAdapter = new ChangeListAdapter(this);
// Set onClick listener and add adapter to recycler view.
mRecentChangeAdapter.setOnClickListener(
new ItemClickListener() {
@Override
public void onItemClick(DiskEvent diskEvent) {
Log.v(TAG, "User clicked item with title \'" + diskEvent.data.path + "\'");
/**
* Future improvement:
* Collapse texts to the first three lines and open a DialogFragment
* if the user clicks an item from the list.
*/
}
}
);
mRecyclerView.setAdapter(mRecentChangeAdapter);
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
super.onServiceConnected(componentName, iBinder);
SyncthingServiceBinder syncthingServiceBinder = (SyncthingServiceBinder) iBinder;
syncthingServiceBinder.getService().registerOnServiceStateChangeListener(this);
}
@Override
public void onServiceStateChange(SyncthingService.State newState) {
Log.v(TAG, "onServiceStateChange(" + newState + ")");
mServiceState = newState;
if (newState == SyncthingService.State.ACTIVE) {
onTimerEvent();
}
}
@Override
protected void onDestroy() {
SyncthingService syncthingService = getService();
if (syncthingService != null) {
syncthingService.unregisterOnServiceStateChangeListener(this);
}
super.onDestroy();
}
private void onTimerEvent() {
if (isFinishing()) {
return;
}
if (mServiceState != SyncthingService.State.ACTIVE) {
return;
}
SyncthingService syncthingService = getService();
if (syncthingService == null) {
Log.e(TAG, "syncthingService == null");
return;
}
RestApi restApi = syncthingService.getApi();
if (restApi == null) {
Log.e(TAG, "restApi == null");
return;
}
mDevices = restApi.getDevices(true);
Log.v(TAG, "Querying disk events");
restApi.getDiskEvents(DISK_EVENT_LIMIT, this::onReceiveDiskEvents);
}
private void onReceiveDiskEvents(List<DiskEvent> diskEvents) {
Log.v(TAG, "onReceiveDiskEvents");
if (isFinishing()) {
return;
}
mRecentChangeAdapter.clear();
for (DiskEvent diskEvent : diskEvents) {
if (diskEvent.data != null) {
// Replace "modifiedBy" partial device ID by readable device name.
if (!TextUtils.isEmpty(diskEvent.data.modifiedBy)) {
for (Device device : mDevices) {
if (diskEvent.data.modifiedBy.equals(device.deviceID.substring(0, diskEvent.data.modifiedBy.length()))) {
diskEvent.data.modifiedBy = device.getDisplayName();
break;
}
}
}
mRecentChangeAdapter.add(diskEvent);
}
}
mRecentChangeAdapter.notifyDataSetChanged();
}
}

View file

@ -17,6 +17,7 @@ import android.widget.Toast;
import com.google.common.collect.ImmutableMap;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.activities.MainActivity;
import com.nutomic.syncthingandroid.activities.RecentChangesActivity;
import com.nutomic.syncthingandroid.activities.SettingsActivity;
import com.nutomic.syncthingandroid.activities.TipsAndTricksActivity;
import com.nutomic.syncthingandroid.activities.WebGuiActivity;
@ -44,6 +45,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
*/
private TextView mVersion = null;
private TextView mDrawerActionShowQrCode;
private TextView mDrawerRecentChanges;
private TextView mDrawerActionWebGui;
private TextView mDrawerActionImportExport;
private TextView mDrawerActionRestart;
@ -91,6 +93,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
mVersion = view.findViewById(R.id.version);
mDrawerActionShowQrCode = view.findViewById(R.id.drawerActionShowQrCode);
mDrawerRecentChanges = view.findViewById(R.id.drawerActionRecentChanges);
mDrawerActionWebGui = view.findViewById(R.id.drawerActionWebGui);
mDrawerActionImportExport = view.findViewById(R.id.drawerActionImportExport);
mDrawerActionRestart = view.findViewById(R.id.drawerActionRestart);
@ -100,6 +103,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
// Add listeners to buttons.
mDrawerActionShowQrCode.setOnClickListener(this);
mDrawerRecentChanges.setOnClickListener(this);
mDrawerActionWebGui.setOnClickListener(this);
mDrawerActionImportExport.setOnClickListener(this);
mDrawerActionRestart.setOnClickListener(this);
@ -134,6 +138,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
// Show buttons if syncthing is running.
mVersion.setVisibility(synthingRunning ? View.VISIBLE : View.GONE);
mDrawerActionShowQrCode.setVisibility(synthingRunning ? View.VISIBLE : View.GONE);
mDrawerRecentChanges.setVisibility(synthingRunning ? View.VISIBLE : View.GONE);
mDrawerActionWebGui.setVisibility(synthingRunning ? View.VISIBLE : View.GONE);
mDrawerActionRestart.setVisibility(synthingRunning ? View.VISIBLE : View.GONE);
mDrawerTipsAndTricks.setVisibility(View.VISIBLE);
@ -171,6 +176,10 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
case R.id.drawerActionShowQrCode:
showQrCode();
break;
case R.id.drawerActionRecentChanges:
startActivity(new Intent(mActivity, RecentChangesActivity.class));
mActivity.closeDrawer();
break;
case R.id.drawerActionWebGui:
startActivity(new Intent(mActivity, WebGuiActivity.class));
mActivity.closeDrawer();

View file

@ -26,6 +26,7 @@ public class GetRequest extends ApiRequest {
public static final String URI_DEVICEID = "/rest/svc/deviceid";
public static final String URI_REPORT = "/rest/svc/report";
public static final String URI_EVENTS = "/rest/events";
public static final String URI_EVENTS_DISK = "/rest/events/disk";
public GetRequest(Context context, URL url, String path, String apiKey,
@Nullable Map<String, String> params, OnSuccessListener listener) {

View file

@ -0,0 +1,15 @@
package com.nutomic.syncthingandroid.model;
/**
* REST API endpoint "/rest/events/disk"
*/
public class DiskEvent {
public long id = 0;
public long globalID = 0;
public String time = "";
// type = {"LocalChangeDetected", "RemoteChangeDetected"}
public String type = "";
public DiskEventData data;
}

View file

@ -0,0 +1,18 @@
package com.nutomic.syncthingandroid.model;
/**
* REST API endpoint "/rest/events/disk"
*/
public class DiskEventData {
// action = {"added", "deleted", "modified"}
public String action = "";
public String folder = "";
public String folderID = "";
public String label = "";
public String modifiedBy = "";
public String path = "";
// type = {"file", "dir"}
public String type = "";
}

View file

@ -27,6 +27,7 @@ import com.nutomic.syncthingandroid.model.Completion;
import com.nutomic.syncthingandroid.model.CompletionInfo;
import com.nutomic.syncthingandroid.model.Connections;
import com.nutomic.syncthingandroid.model.Device;
import com.nutomic.syncthingandroid.model.DiskEvent;
import com.nutomic.syncthingandroid.model.Event;
import com.nutomic.syncthingandroid.model.Folder;
import com.nutomic.syncthingandroid.model.FolderIgnoreList;
@ -42,6 +43,7 @@ import com.nutomic.syncthingandroid.service.Constants;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@ -671,6 +673,30 @@ public class RestApi {
});
}
/**
* Requests and parses information about recent changes.
*/
public void getDiskEvents(int limit, OnResultListener1<List<DiskEvent>> listener) {
new GetRequest(
mContext, mUrl,
GetRequest.URI_EVENTS_DISK, mApiKey,
ImmutableMap.of("limit", Integer.toString(limit)),
result -> {
List<DiskEvent> diskEvents = new ArrayList<>();
try {
JsonArray jsonDiskEvents = new JsonParser().parse(result).getAsJsonArray();
for (int i = jsonDiskEvents.size()-1; i >= 0; i--) {
JsonElement jsonDiskEvent = jsonDiskEvents.get(i);
diskEvents.add(new Gson().fromJson(jsonDiskEvent, DiskEvent.class));
}
listener.onResult(diskEvents);
} catch (Exception e) {
Log.e(TAG, "getDiskEvents: Parsing REST API result failed. result=" + result);
}
}
);
}
/**
* Listener for {@link #getEvents}.
*/

View file

@ -0,0 +1,177 @@
package com.nutomic.syncthingandroid.views;
import android.annotation.TargetApi;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.support.v7.widget.RecyclerView;
import android.net.Uri;
import android.os.Build;
// import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.model.DiskEvent;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Locale;
public class ChangeListAdapter extends RecyclerView.Adapter<ChangeListAdapter.ViewHolder> {
// private static final String TAG = "ChangeListAdapter";
private final Context mContext;
private final Resources mResources;
private ArrayList<DiskEvent> mChangeData = new ArrayList<DiskEvent>();
private ItemClickListener mOnClickListener;
private LayoutInflater mLayoutInflater;
public interface ItemClickListener {
void onItemClick(DiskEvent diskEvent);
}
public ChangeListAdapter(Context context) {
mContext = context;
mResources = mContext.getResources();
mLayoutInflater = LayoutInflater.from(mContext);
}
public void clear() {
mChangeData.clear();
}
public void add(DiskEvent diskEvent) {
mChangeData.add(diskEvent);
}
public void setOnClickListener(ItemClickListener onClickListener) {
mOnClickListener = onClickListener;
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public ImageView typeIcon;
public TextView filename;
public TextView folderPath;
public TextView modifiedByDevice;
public TextView dateTime;
public View layout;
public ViewHolder(View view) {
super(view);
typeIcon = view.findViewById(R.id.typeIcon);
filename = view.findViewById(R.id.filename);
folderPath = view.findViewById(R.id.folderPath);
modifiedByDevice = view.findViewById(R.id.modifiedByDevice);
dateTime = view.findViewById(R.id.dateTime);
view.setOnClickListener(this);
}
@Override
public void onClick(View view) {
int position = getAdapterPosition();
DiskEvent diskEvent = mChangeData.get(position);
if (mOnClickListener != null) {
mOnClickListener.onItemClick(diskEvent);
}
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mLayoutInflater.inflate(R.layout.item_recent_change, parent, false);
return new ViewHolder(view);
}
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
DiskEvent diskEvent = mChangeData.get(position);
// Separate path and filename.
Uri uri = Uri.parse(diskEvent.data.path);
String filename = uri.getLastPathSegment();
String path = getPathFromFullFN(diskEvent.data.path);
// Decide which icon to show.
int drawableId = R.drawable.ic_help_outline_black_24dp;
switch (diskEvent.data.type) {
case "dir":
switch (diskEvent.data.action) {
case "added":
drawableId = R.drawable.ic_folder_add_black_24dp;
break;
case "deleted":
drawableId = R.drawable.ic_folder_delete_black_24dp;
break;
case "modified":
drawableId = R.drawable.ic_folder_edit_black_24dp;
break;
default:
}
break;
case "file":
switch (diskEvent.data.action) {
case "added":
drawableId = R.drawable.ic_file_add_black_24dp;
break;
case "deleted":
drawableId = R.drawable.ic_file_remove_black_24dp;
break;
case "modified":
drawableId = R.drawable.ic_file_edit_black_24dp;
break;
default:
}
break;
default:
}
viewHolder.typeIcon.setImageResource(drawableId);
// Fill text views.
viewHolder.filename.setText(filename);
viewHolder.folderPath.setText(diskEvent.data.label + File.separator + path);
viewHolder.modifiedByDevice.setText(mResources.getString(R.string.modified_by_device, diskEvent.data.modifiedBy));
// Convert dateTime to readable localized string.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
viewHolder.dateTime.setText(mResources.getString(R.string.modification_time, diskEvent.time));
} else {
viewHolder.dateTime.setText(mResources.getString(R.string.modification_time, formatDateTime(diskEvent.time)));
}
}
@Override
public int getItemCount() {
return mChangeData.size();
}
/**
* Converts dateTime to readable localized string.
*/
@TargetApi(26)
private String formatDateTime(String dateTime) {
ZonedDateTime parsedDateTime = ZonedDateTime.parse(dateTime);
ZonedDateTime zonedDateTime = parsedDateTime.withZoneSameInstant(ZoneId.systemDefault());
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
return formatter.format(zonedDateTime);
}
private String getPathFromFullFN(String fullFN) {
int index = fullFN.lastIndexOf('/');
if (index > 0) {
return fullFN.substring(0, index);
}
return "";
}
}

View file

@ -1,4 +1,5 @@
Enhancements
* Added "Recent changes" UI [NEW]
* Specify sync conditions differently for each folder, device [NEW]
* Added offline 'tips & tricks' content [NEW]
* UI explains why syncthing is running (or not)
@ -6,6 +7,5 @@ Enhancements
Fixes
* Fixed the "battery eater"
* Android 8 and 9 support
* Fixed phone plugged to charger detection
Maintenance
* Updated syncthing to v0.14.51 (receiveOnly folders)

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/widget_toolbar" />
<android.support.v7.widget.RecyclerView
android:id="@+id/changes_recycler_view"
android:paddingTop="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</RelativeLayout>

View file

@ -103,6 +103,17 @@
android:focusable="true"
android:text="@string/show_device_id" />
<TextView
android:id="@+id/drawerActionRecentChanges"
style="@style/Widget.Syncthing.TextView.Label"
android:layout_width="match_parent"
android:layout_height="48dp"
android:drawableLeft="@drawable/ic_history_black_24dp_active"
android:drawableStart="@drawable/ic_history_black_24dp_active"
android:text="@string/recent_changes_title"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/drawerActionWebGui"
style="@style/Widget.Syncthing.TextView.Label"

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="6dip"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/typeIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:layout_marginRight="6dip"
android:contentDescription="@string/generic_help"
android:gravity="top"
android:src="@drawable/ic_help_outline_black_24dp" />
<TextView
android:id="@+id/filename"
android:layout_marginTop="-25dp"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp" />
<TextView
android:id="@+id/folderPath"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
<TextView
android:id="@+id/modifiedByDevice"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
<TextView
android:id="@+id/dateTime"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
</LinearLayout>

View file

@ -306,6 +306,13 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
<string name="tip_huawei_device_disconnected_text">Falls der Computer das Huawei-Gerät permanent als \"getrennt\" meldet, öffne die Syncthing-Oberfläche auf dem Computer. Gehe zu \"Externe Geräte\", klappe den Eintrag des Mobilgeräts aus, klicke \"Bearbeiten\", wechsle zu \"Erweitert\". Trage die IP-Adresse deines Mobilgeräts in \"Adressen\" wie folgt ein:\ntcp4://%1$s, dynamic\nFunktioniert sicher auf: Huawei P10</string>
<string name="tip_phone_ip_address_syntax">TELEFON_IP_ADRESSE</string>
<!-- RecentChangesActivity -->
<string name="recent_changes_title">Letzte Änderungen</string>
<string name="modified_by_device">Gerät: %1$s</string>
<string name="modification_time">Zeit: %1$s</string>
<!-- WebGuiActivity -->
<!-- Title of the web gui activity -->

View file

@ -306,6 +306,13 @@ Please report any problems you encounter via Github.</string>
<string name="tip_huawei_device_disconnected_text">If your desktop constantly reports your Huawei device as disconnected, open Syncthing UI of your desktop. Go to \'remote devices\', expand the phone\'s entry, click \'Edit\', switch to \'Advanced\'. Put your phone IP address into \'Addresses\' like this:\ntcp4://%1$s, dynamic\nConfirmed working for: Huawei P10</string>
<string name="tip_phone_ip_address_syntax">PHONE_IP_ADDRESS</string>
<!-- RecentChangesActivity -->
<string name="recent_changes_title">Recent changes</string>
<string name="modified_by_device">Device: %1$s</string>
<string name="modification_time">Time: %1$s</string>
<!-- WebGuiActivity -->
<!-- Title of the web gui activity -->