Merge branch 'master' into bg_sync

This commit is contained in:
vishnukvmd 2021-11-25 15:35:44 +05:30
commit 3f9137481f
7 changed files with 485 additions and 79 deletions

View file

@ -124,7 +124,7 @@ class Configuration {
if (SyncService.instance.isSyncInProgress()) {
SyncService.instance.stopSync();
try {
await SyncService.instance.existingSync();
await SyncService.instance.existingSync().timeout(Duration(seconds: 5));
} catch (e) {
// ignore
}

140
lib/models/sessions.dart Normal file
View file

@ -0,0 +1,140 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
class Sessions {
final List<Session> sessions;
Sessions(
this.sessions,
);
Sessions copyWith({
List<Session> sessions,
}) {
return Sessions(
sessions ?? this.sessions,
);
}
Map<String, dynamic> toMap() {
return {
'sessions': sessions?.map((x) => x.toMap())?.toList(),
};
}
factory Sessions.fromMap(Map<String, dynamic> map) {
return Sessions(
List<Session>.from(map['sessions']?.map((x) => Session.fromMap(x))),
);
}
String toJson() => json.encode(toMap());
factory Sessions.fromJson(String source) =>
Sessions.fromMap(json.decode(source));
@override
String toString() => 'Sessions(sessions: $sessions)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Sessions && listEquals(other.sessions, sessions);
}
@override
int get hashCode => sessions.hashCode;
}
class Session {
final String token;
final int creationTime;
final String ip;
final String ua;
final String prettyUA;
final int lastUsedTime;
Session(
this.token,
this.creationTime,
this.ip,
this.ua,
this.prettyUA,
this.lastUsedTime,
);
Session copyWith({
String token,
int creationTime,
String ip,
String ua,
String prettyUA,
int lastUsedTime,
}) {
return Session(
token ?? this.token,
creationTime ?? this.creationTime,
ip ?? this.ip,
ua ?? this.ua,
prettyUA ?? this.prettyUA,
lastUsedTime ?? this.lastUsedTime,
);
}
Map<String, dynamic> toMap() {
return {
'token': token,
'creationTime': creationTime,
'ip': ip,
'ua': ua,
'prettyUA': prettyUA,
'lastUsedTime': lastUsedTime,
};
}
factory Session.fromMap(Map<String, dynamic> map) {
return Session(
map['token'],
map['creationTime'],
map['ip'],
map['ua'],
map['prettyUA'],
map['lastUsedTime'],
);
}
String toJson() => json.encode(toMap());
factory Session.fromJson(String source) =>
Session.fromMap(json.decode(source));
@override
String toString() {
return 'Session(token: $token, creationTime: $creationTime, ip: $ip, ua: $ua, prettyUA: $prettyUA, lastUsedTime: $lastUsedTime)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Session &&
other.token == token &&
other.creationTime == creationTime &&
other.ip == ip &&
other.ua == ua &&
other.prettyUA == prettyUA &&
other.lastUsedTime == lastUsedTime;
}
@override
int get hashCode {
return token.hashCode ^
creationTime.hashCode ^
ip.hashCode ^
ua.hashCode ^
prettyUA.hashCode ^
lastUsedTime.hashCode;
}
}

View file

@ -15,6 +15,7 @@ import 'package:photos/events/user_details_changed_event.dart';
import 'package:photos/models/key_attributes.dart';
import 'package:photos/models/key_gen_result.dart';
import 'package:photos/models/public_key.dart';
import 'package:photos/models/sessions.dart';
import 'package:photos/models/set_keys_request.dart';
import 'package:photos/models/set_recovery_key_request.dart';
import 'package:photos/models/user_details.dart';
@ -121,6 +122,40 @@ class UserService {
}
}
Future<Sessions> getActiveSessions() async {
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/users/sessions",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
},
),
);
return Sessions.fromMap(response.data);
} on DioError catch (e) {
_logger.info(e);
rethrow;
}
}
Future<void> terminateSession(String token) async {
try {
await _dio.delete(_config.getHttpEndpoint() + "/users/session",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
},
),
queryParameters: {
"token": token,
});
} on DioError catch (e) {
_logger.info(e);
rethrow;
}
}
Future<void> logout(BuildContext context) async {
final dialog = createProgressDialog(context, "logging out...");
await dialog.show();

View file

@ -182,9 +182,12 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
file.getDisplayName(),
Flexible(
child: Text(
file.getDisplayName(),
),
),
Padding(padding: EdgeInsets.all(8)),
Icon(
Icons.edit,
color: Colors.white.withOpacity(0.85),

199
lib/ui/sessions_page.dart Normal file
View file

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/sessions.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/utils/date_time_util.dart';
import 'package:photos/utils/dialog_util.dart';
class SessionsPage extends StatefulWidget {
SessionsPage({Key key}) : super(key: key);
@override
_SessionsPageState createState() => _SessionsPageState();
}
class _SessionsPageState extends State<SessionsPage> {
Sessions _sessions;
@override
void initState() {
_fetchActiveSessions();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("active sessions"),
),
body: _getBody(),
);
}
Widget _getBody() {
if (_sessions == null) {
return Center(child: loadWidget);
}
List<Widget> rows = [];
for (final session in _sessions.sessions) {
rows.add(_getSessionWidget(session));
}
return SingleChildScrollView(
child: Column(
children: rows,
),
);
}
Widget _getSessionWidget(Session session) {
final lastUsedTime =
DateTime.fromMicrosecondsSinceEpoch(session.lastUsedTime);
return Column(
children: [
InkWell(
onTap: () async {
_showSessionTerminationDialog(session);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_getUAWidget(session),
Padding(padding: EdgeInsets.all(4)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
session.ip,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
),
Padding(padding: EdgeInsets.all(8)),
Flexible(
child: Text(
getFormattedTime(lastUsedTime),
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
),
],
),
],
),
),
),
Divider(),
],
);
}
Future<void> _terminateSession(Session session) async {
final dialog = createProgressDialog(context, "please wait...");
await dialog.show();
await UserService.instance.terminateSession(session.token);
await _fetchActiveSessions();
await dialog.hide();
}
Future<void> _fetchActiveSessions() async {
_sessions = await UserService.instance.getActiveSessions();
_sessions.sessions.sort((first, second) {
return second.lastUsedTime.compareTo(first.lastUsedTime);
});
setState(() {});
}
void _showSessionTerminationDialog(Session session) {
final isLoggingOutFromThisDevice =
session.token == Configuration.instance.getToken();
Widget text;
if (isLoggingOutFromThisDevice) {
text = Text(
"this will log you out of this device!",
);
} else {
text = SingleChildScrollView(
child: Column(
children: [
Text(
"this will log you out of the following device:",
),
Padding(padding: EdgeInsets.all(8)),
Text(
session.ua,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 14,
),
),
],
),
);
}
AlertDialog alert = AlertDialog(
title: Text("terminate session?"),
content: text,
actions: [
TextButton(
child: Text(
"terminate",
style: TextStyle(
color: Colors.red,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
if (isLoggingOutFromThisDevice) {
await UserService.instance.logout(context);
} else {
_terminateSession(session);
}
},
),
TextButton(
child: Text(
"cancel",
style: TextStyle(
color: isLoggingOutFromThisDevice
? Theme.of(context).buttonColor
: Colors.white,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
Widget _getUAWidget(Session session) {
if (session.token == Configuration.instance.getToken()) {
return Text(
"this device",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).buttonColor,
),
);
}
return Text(session.prettyUA);
}
}

View file

@ -9,7 +9,9 @@ import 'package:photos/events/two_factor_status_change_event.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/app_lock.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/sessions_page.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/auth_util.dart';
import 'package:photos/utils/toast_util.dart';
@ -128,86 +130,113 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
),
]);
if (Platform.isAndroid) {
children.addAll([
Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
Padding(padding: EdgeInsets.all(4)),
SizedBox(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("hide from recents"),
Switch(
value: _config.shouldHideFromRecents(),
onChanged: (value) async {
if (value) {
AlertDialog alert = AlertDialog(
title: Text("hide from recents?"),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"hiding from the task switcher will prevent you from taking screenshots in this app.",
style: TextStyle(
height: 1.5,
children.addAll(
[
Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
Padding(padding: EdgeInsets.all(4)),
SizedBox(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("hide from recents"),
Switch(
value: _config.shouldHideFromRecents(),
onChanged: (value) async {
if (value) {
AlertDialog alert = AlertDialog(
title: Text("hide from recents?"),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"hiding from the task switcher will prevent you from taking screenshots in this app.",
style: TextStyle(
height: 1.5,
),
),
),
Padding(padding: EdgeInsets.all(8)),
Text(
"are you sure?",
style: TextStyle(
height: 1.5,
Padding(padding: EdgeInsets.all(8)),
Text(
"are you sure?",
style: TextStyle(
height: 1.5,
),
),
),
],
],
),
),
),
actions: [
TextButton(
child:
Text("no", style: TextStyle(color: Colors.white)),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
},
),
TextButton(
child: Text("yes",
style: TextStyle(
color: Colors.white.withOpacity(0.8))),
onPressed: () async {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
await _config.setShouldHideFromRecents(true);
await FlutterWindowManager.addFlags(
FlutterWindowManager.FLAG_SECURE);
setState(() {});
},
),
],
);
actions: [
TextButton(
child: Text("no",
style: TextStyle(color: Colors.white)),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
},
),
TextButton(
child: Text("yes",
style: TextStyle(
color: Colors.white.withOpacity(0.8))),
onPressed: () async {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
await _config.setShouldHideFromRecents(true);
await FlutterWindowManager.addFlags(
FlutterWindowManager.FLAG_SECURE);
setState(() {});
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
} else {
await _config.setShouldHideFromRecents(false);
await FlutterWindowManager.clearFlags(
FlutterWindowManager.FLAG_SECURE);
setState(() {});
}
},
),
],
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
} else {
await _config.setShouldHideFromRecents(false);
await FlutterWindowManager.clearFlags(
FlutterWindowManager.FLAG_SECURE);
setState(() {});
}
},
),
],
),
),
),
]);
Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
Padding(padding: EdgeInsets.all(2)),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
AppLock.of(context).setEnabled(false);
final result = await requestAuthentication();
AppLock.of(context)
.setEnabled(Configuration.instance.shouldShowLockScreen());
if (!result) {
showToast("please authenticate to view your active sessions");
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return SessionsPage();
},
),
);
},
child: SettingsTextItem(
text: "active sessions", icon: Icons.navigate_next),
),
],
);
}
return Column(
children: children,

View file

@ -11,7 +11,7 @@ description: ente photos application
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.3.48+258
version: 0.4.1+261
environment:
sdk: ">=2.10.0 <3.0.0"