Merge branch 'main' into add_location_screen
This commit is contained in:
commit
8283612f0e
33
.github/workflows/crowdin.yml
vendored
Normal file
33
.github/workflows/crowdin.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: Sync crowdin translation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'lib/l10n/app_en.arb'
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v1
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
localization_branch_name: l10n_translations
|
||||
create_pull_request: true
|
||||
skip_untranslated_strings: true
|
||||
pull_request_title: 'New Translations'
|
||||
pull_request_body: 'New translations via [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||
pull_request_base_branch_name: 'main'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
1001
assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt
Normal file
1001
assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt
Normal file
File diff suppressed because it is too large
Load diff
BIN
assets/models/mobilenet/mobilenet_v1_1.0_224_quant.tflite
Normal file
BIN
assets/models/mobilenet/mobilenet_v1_1.0_224_quant.tflite
Normal file
Binary file not shown.
30
assets/models/scenes/labels.txt
Normal file
30
assets/models/scenes/labels.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
waterfall
|
||||
snow
|
||||
landscape
|
||||
underwater
|
||||
architecture
|
||||
sunset / sunrise
|
||||
blue sky
|
||||
cloudy sky
|
||||
greenery
|
||||
autumn leaves
|
||||
potrait
|
||||
flower
|
||||
night shot
|
||||
stage concert
|
||||
fireworks
|
||||
candle light
|
||||
neon lights
|
||||
indoor
|
||||
backlight
|
||||
text documents
|
||||
qr images
|
||||
group potrait
|
||||
computer screens
|
||||
kids
|
||||
dog
|
||||
cat
|
||||
macro
|
||||
food
|
||||
beach
|
||||
mountain
|
BIN
assets/models/scenes/model.tflite
Normal file
BIN
assets/models/scenes/model.tflite
Normal file
Binary file not shown.
6
crowdin.yml
Normal file
6
crowdin.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
|
||||
files:
|
||||
- source: /lib/l10n/app_en.arb
|
||||
translation: /lib/l10n/app_%two_letters_code%.arb
|
1
lib/l10n/app_de.arb
Normal file
1
lib/l10n/app_de.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
6
lib/l10n/app_fr.arb
Normal file
6
lib/l10n/app_fr.arb
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"sign_up": "inscription",
|
||||
"@sign_up": {
|
||||
"description": "Text on the sign up button used during registration"
|
||||
}
|
||||
}
|
1
lib/l10n/app_it.arb
Normal file
1
lib/l10n/app_it.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
lib/l10n/app_nl.arb
Normal file
1
lib/l10n/app_nl.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -131,7 +131,8 @@ class BonusDetails {
|
|||
|
||||
factory BonusDetails.fromJson(Map<String, dynamic> json) => BonusDetails(
|
||||
referralStats: List<ReferralStat>.from(
|
||||
json["referralStats"].map((x) => ReferralStat.fromJson(x))),
|
||||
json["referralStats"].map((x) => ReferralStat.fromJson(x)),
|
||||
),
|
||||
bonuses:
|
||||
List<Bonus>.from(json["bonuses"].map((x) => Bonus.fromJson(x))),
|
||||
refCount: json["refCount"],
|
||||
|
|
|
@ -11,6 +11,10 @@ class DeviceCollection {
|
|||
int? collectionID;
|
||||
File? thumbnail;
|
||||
|
||||
bool hasCollectionID() {
|
||||
return collectionID != null && collectionID! != -1;
|
||||
}
|
||||
|
||||
DeviceCollection(
|
||||
this.id,
|
||||
this.name, {
|
||||
|
|
|
@ -137,7 +137,10 @@ class FavoritesService {
|
|||
}
|
||||
|
||||
Future<void> updateFavorites(
|
||||
BuildContext context, List<File> files, bool favFlag) async {
|
||||
BuildContext context,
|
||||
List<File> files,
|
||||
bool favFlag,
|
||||
) async {
|
||||
final int currentUserID = Configuration.instance.getUserID()!;
|
||||
if (files.any((f) => f.uploadedFileID == null)) {
|
||||
throw AssertionError("Can only favorite uploaded items");
|
||||
|
|
|
@ -4,18 +4,20 @@ import "dart:typed_data";
|
|||
import "package:logging/logging.dart";
|
||||
import "package:photos/services/object_detection/models/predictions.dart";
|
||||
import 'package:photos/services/object_detection/models/recognition.dart';
|
||||
import "package:photos/services/object_detection/tflite/classifier.dart";
|
||||
import 'package:photos/services/object_detection/tflite/cocossd_classifier.dart';
|
||||
import "package:photos/services/object_detection/tflite/mobilenet_classifier.dart";
|
||||
import "package:photos/services/object_detection/tflite/scene_classifier.dart";
|
||||
import "package:photos/services/object_detection/utils/isolate_utils.dart";
|
||||
|
||||
class ObjectDetectionService {
|
||||
static const scoreThreshold = 0.6;
|
||||
static const scoreThreshold = 0.5;
|
||||
|
||||
final _logger = Logger("ObjectDetectionService");
|
||||
|
||||
/// Instance of [ObjectClassifier]
|
||||
late ObjectClassifier _classifier;
|
||||
late CocoSSDClassifier _objectClassifier;
|
||||
late MobileNetClassifier _mobileNetClassifier;
|
||||
late SceneClassifier _sceneClassifier;
|
||||
|
||||
/// Instance of [IsolateUtils]
|
||||
late IsolateUtils _isolateUtils;
|
||||
|
||||
ObjectDetectionService._privateConstructor();
|
||||
|
@ -23,7 +25,9 @@ class ObjectDetectionService {
|
|||
Future<void> init() async {
|
||||
_isolateUtils = IsolateUtils();
|
||||
await _isolateUtils.start();
|
||||
_classifier = ObjectClassifier();
|
||||
_objectClassifier = CocoSSDClassifier();
|
||||
_mobileNetClassifier = MobileNetClassifier();
|
||||
_sceneClassifier = SceneClassifier();
|
||||
}
|
||||
|
||||
static ObjectDetectionService instance =
|
||||
|
@ -31,18 +35,10 @@ class ObjectDetectionService {
|
|||
|
||||
Future<List<String>> predict(Uint8List bytes) async {
|
||||
try {
|
||||
final isolateData = IsolateData(
|
||||
bytes,
|
||||
_classifier.interpreter.address,
|
||||
_classifier.labels,
|
||||
);
|
||||
final predictions = await _inference(isolateData);
|
||||
final Set<String> results = {};
|
||||
for (final Recognition result in predictions.recognitions) {
|
||||
if (result.score > scoreThreshold) {
|
||||
results.add(result.label);
|
||||
}
|
||||
}
|
||||
final results = <String>{};
|
||||
results.addAll(await _getObjects(bytes));
|
||||
results.addAll(await _getMobileNetResults(bytes));
|
||||
results.addAll(await _getSceneResults(bytes));
|
||||
return results.toList();
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
|
@ -50,6 +46,54 @@ class ObjectDetectionService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _getObjects(Uint8List bytes) async {
|
||||
final isolateData = IsolateData(
|
||||
bytes,
|
||||
_objectClassifier.interpreter.address,
|
||||
_objectClassifier.labels,
|
||||
ClassifierType.cocossd,
|
||||
);
|
||||
return _getPredictions(isolateData);
|
||||
}
|
||||
|
||||
Future<List<String>> _getMobileNetResults(Uint8List bytes) async {
|
||||
final isolateData = IsolateData(
|
||||
bytes,
|
||||
_mobileNetClassifier.interpreter.address,
|
||||
_mobileNetClassifier.labels,
|
||||
ClassifierType.mobilenet,
|
||||
);
|
||||
return _getPredictions(isolateData);
|
||||
}
|
||||
|
||||
Future<List<String>> _getSceneResults(Uint8List bytes) async {
|
||||
final isolateData = IsolateData(
|
||||
bytes,
|
||||
_sceneClassifier.interpreter.address,
|
||||
_sceneClassifier.labels,
|
||||
ClassifierType.scenes,
|
||||
);
|
||||
return _getPredictions(isolateData);
|
||||
}
|
||||
|
||||
Future<List<String>> _getPredictions(IsolateData isolateData) async {
|
||||
final predictions = await _inference(isolateData);
|
||||
final Set<String> results = {};
|
||||
for (final Recognition result in predictions.recognitions) {
|
||||
if (result.score > scoreThreshold) {
|
||||
results.add(result.label);
|
||||
}
|
||||
}
|
||||
_logger.info(
|
||||
"Time taken for " +
|
||||
isolateData.type.toString() +
|
||||
": " +
|
||||
predictions.stats.totalElapsedTime.toString() +
|
||||
"ms",
|
||||
);
|
||||
return results.toList();
|
||||
}
|
||||
|
||||
/// Runs inference in another isolate
|
||||
Future<Predictions> _inference(IsolateData isolateData) async {
|
||||
final responsePort = ReceivePort();
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import 'dart:math';
|
||||
import "dart:math";
|
||||
|
||||
import 'package:image/image.dart' as imageLib;
|
||||
import 'package:image/image.dart' as image_lib;
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/services/object_detection/models/predictions.dart';
|
||||
import 'package:photos/services/object_detection/models/recognition.dart';
|
||||
import "package:photos/services/object_detection/models/stats.dart";
|
||||
import "package:photos/services/object_detection/models/predictions.dart";
|
||||
import "package:tflite_flutter/tflite_flutter.dart";
|
||||
import "package:tflite_flutter_helper/tflite_flutter_helper.dart";
|
||||
|
||||
/// Classifier
|
||||
class ObjectClassifier {
|
||||
final _logger = Logger("Classifier");
|
||||
abstract class Classifier {
|
||||
// Path to the model
|
||||
String get modelPath;
|
||||
|
||||
// Path to the labels
|
||||
String get labelPath;
|
||||
|
||||
// Input size expected by the model (for eg. width = height = 224)
|
||||
int get inputSize;
|
||||
|
||||
// Logger implementation for the specific classifier
|
||||
Logger get logger;
|
||||
|
||||
Predictions? predict(image_lib.Image image);
|
||||
|
||||
/// Instance of Interpreter
|
||||
late Interpreter _interpreter;
|
||||
|
@ -18,44 +27,30 @@ class ObjectClassifier {
|
|||
/// Labels file loaded as list
|
||||
late List<String> _labels;
|
||||
|
||||
/// Input size of image (height = width = 300)
|
||||
static const int inputSize = 300;
|
||||
|
||||
/// Result score threshold
|
||||
static const double threshold = 0.5;
|
||||
|
||||
static const String modelFileName = "detect.tflite";
|
||||
static const String labelFileName = "labelmap.txt";
|
||||
|
||||
/// [ImageProcessor] used to pre-process the image
|
||||
ImageProcessor? imageProcessor;
|
||||
|
||||
/// Padding the image to transform into square
|
||||
late int padSize;
|
||||
|
||||
/// Shapes of output tensors
|
||||
late List<List<int>> _outputShapes;
|
||||
|
||||
/// Types of output tensors
|
||||
late List<TfLiteType> _outputTypes;
|
||||
|
||||
/// Number of results to show
|
||||
static const int numResults = 10;
|
||||
/// Gets the interpreter instance
|
||||
Interpreter get interpreter => _interpreter;
|
||||
|
||||
ObjectClassifier({
|
||||
Interpreter? interpreter,
|
||||
List<String>? labels,
|
||||
}) {
|
||||
loadModel(interpreter);
|
||||
loadLabels(labels);
|
||||
}
|
||||
/// Gets the loaded labels
|
||||
List<String> get labels => _labels;
|
||||
|
||||
/// Gets the output shapes
|
||||
List<List<int>> get outputShapes => _outputShapes;
|
||||
|
||||
/// Gets the output types
|
||||
List<TfLiteType> get outputTypes => _outputTypes;
|
||||
|
||||
/// Loads interpreter from asset
|
||||
void loadModel(Interpreter? interpreter) async {
|
||||
try {
|
||||
_interpreter = interpreter ??
|
||||
await Interpreter.fromAsset(
|
||||
"models/" + modelFileName,
|
||||
modelPath,
|
||||
options: InterpreterOptions()..threads = 4,
|
||||
);
|
||||
final outputTensors = _interpreter.getOutputTensors();
|
||||
|
@ -65,115 +60,30 @@ class ObjectClassifier {
|
|||
_outputShapes.add(tensor.shape);
|
||||
_outputTypes.add(tensor.type);
|
||||
});
|
||||
_logger.info("Interpreter initialized");
|
||||
logger.info("Interpreter initialized");
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error while creating interpreter", e, s);
|
||||
logger.severe("Error while creating interpreter", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads labels from assets
|
||||
void loadLabels(List<String>? labels) async {
|
||||
try {
|
||||
_labels =
|
||||
labels ?? await FileUtil.loadLabels("assets/models/" + labelFileName);
|
||||
_logger.info("Labels initialized");
|
||||
_labels = labels ?? await FileUtil.loadLabels(labelPath);
|
||||
logger.info("Labels initialized");
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error while loading labels", e, s);
|
||||
logger.severe("Error while loading labels", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-process the image
|
||||
TensorImage _getProcessedImage(TensorImage inputImage) {
|
||||
padSize = max(inputImage.height, inputImage.width);
|
||||
imageProcessor ??= ImageProcessorBuilder()
|
||||
TensorImage getProcessedImage(TensorImage inputImage) {
|
||||
final padSize = max(inputImage.height, inputImage.width);
|
||||
final imageProcessor = ImageProcessorBuilder()
|
||||
.add(ResizeWithCropOrPadOp(padSize, padSize))
|
||||
.add(ResizeOp(inputSize, inputSize, ResizeMethod.BILINEAR))
|
||||
.build();
|
||||
inputImage = imageProcessor!.process(inputImage);
|
||||
inputImage = imageProcessor.process(inputImage);
|
||||
return inputImage;
|
||||
}
|
||||
|
||||
/// Runs object detection on the input image
|
||||
Predictions? predict(imageLib.Image image) {
|
||||
final predictStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final preProcessStart = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Create TensorImage from image
|
||||
TensorImage inputImage = TensorImage.fromImage(image);
|
||||
|
||||
// Pre-process TensorImage
|
||||
inputImage = _getProcessedImage(inputImage);
|
||||
|
||||
final preProcessElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - preProcessStart;
|
||||
|
||||
// TensorBuffers for output tensors
|
||||
final outputLocations = TensorBufferFloat(_outputShapes[0]);
|
||||
final outputClasses = TensorBufferFloat(_outputShapes[1]);
|
||||
final outputScores = TensorBufferFloat(_outputShapes[2]);
|
||||
final numLocations = TensorBufferFloat(_outputShapes[3]);
|
||||
|
||||
// Inputs object for runForMultipleInputs
|
||||
// Use [TensorImage.buffer] or [TensorBuffer.buffer] to pass by reference
|
||||
final inputs = [inputImage.buffer];
|
||||
|
||||
// Outputs map
|
||||
final outputs = {
|
||||
0: outputLocations.buffer,
|
||||
1: outputClasses.buffer,
|
||||
2: outputScores.buffer,
|
||||
3: numLocations.buffer,
|
||||
};
|
||||
|
||||
final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// run inference
|
||||
_interpreter.runForMultipleInputs(inputs, outputs);
|
||||
|
||||
final inferenceTimeElapsed =
|
||||
DateTime.now().millisecondsSinceEpoch - inferenceTimeStart;
|
||||
|
||||
// Maximum number of results to show
|
||||
final resultsCount = min(numResults, numLocations.getIntValue(0));
|
||||
|
||||
// Using labelOffset = 1 as ??? at index 0
|
||||
const labelOffset = 1;
|
||||
|
||||
final recognitions = <Recognition>[];
|
||||
|
||||
for (int i = 0; i < resultsCount; i++) {
|
||||
// Prediction score
|
||||
final score = outputScores.getDoubleValue(i);
|
||||
|
||||
// Label string
|
||||
final labelIndex = outputClasses.getIntValue(i) + labelOffset;
|
||||
final label = _labels.elementAt(labelIndex);
|
||||
|
||||
if (score > threshold) {
|
||||
recognitions.add(
|
||||
Recognition(i, label, score),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final predictElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - predictStartTime;
|
||||
_logger.info(recognitions);
|
||||
return Predictions(
|
||||
recognitions,
|
||||
Stats(
|
||||
predictElapsedTime,
|
||||
predictElapsedTime,
|
||||
inferenceTimeElapsed,
|
||||
preProcessElapsedTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the interpreter instance
|
||||
Interpreter get interpreter => _interpreter;
|
||||
|
||||
/// Gets the loaded labels
|
||||
List<String> get labels => _labels;
|
||||
}
|
||||
|
|
115
lib/services/object_detection/tflite/cocossd_classifier.dart
Normal file
115
lib/services/object_detection/tflite/cocossd_classifier.dart
Normal file
|
@ -0,0 +1,115 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:image/image.dart' as image_lib;
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/services/object_detection/models/predictions.dart';
|
||||
import 'package:photos/services/object_detection/models/recognition.dart';
|
||||
import "package:photos/services/object_detection/models/stats.dart";
|
||||
import "package:photos/services/object_detection/tflite/classifier.dart";
|
||||
import "package:tflite_flutter/tflite_flutter.dart";
|
||||
import "package:tflite_flutter_helper/tflite_flutter_helper.dart";
|
||||
|
||||
/// Classifier
|
||||
class CocoSSDClassifier extends Classifier {
|
||||
static final _logger = Logger("CocoSSDClassifier");
|
||||
static const double threshold = 0.5;
|
||||
|
||||
@override
|
||||
String get modelPath => "models/cocossd/model.tflite";
|
||||
|
||||
@override
|
||||
String get labelPath => "assets/models/cocossd/labels.txt";
|
||||
|
||||
@override
|
||||
int get inputSize => 300;
|
||||
|
||||
@override
|
||||
Logger get logger => _logger;
|
||||
|
||||
static const int numResults = 10;
|
||||
|
||||
CocoSSDClassifier({
|
||||
Interpreter? interpreter,
|
||||
List<String>? labels,
|
||||
}) {
|
||||
loadModel(interpreter);
|
||||
loadLabels(labels);
|
||||
}
|
||||
|
||||
@override
|
||||
Predictions? predict(image_lib.Image image) {
|
||||
final predictStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final preProcessStart = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Create TensorImage from image
|
||||
TensorImage inputImage = TensorImage.fromImage(image);
|
||||
|
||||
// Pre-process TensorImage
|
||||
inputImage = getProcessedImage(inputImage);
|
||||
|
||||
final preProcessElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - preProcessStart;
|
||||
|
||||
// TensorBuffers for output tensors
|
||||
final outputLocations = TensorBufferFloat(outputShapes[0]);
|
||||
final outputClasses = TensorBufferFloat(outputShapes[1]);
|
||||
final outputScores = TensorBufferFloat(outputShapes[2]);
|
||||
final numLocations = TensorBufferFloat(outputShapes[3]);
|
||||
|
||||
// Inputs object for runForMultipleInputs
|
||||
// Use [TensorImage.buffer] or [TensorBuffer.buffer] to pass by reference
|
||||
final inputs = [inputImage.buffer];
|
||||
|
||||
// Outputs map
|
||||
final outputs = {
|
||||
0: outputLocations.buffer,
|
||||
1: outputClasses.buffer,
|
||||
2: outputScores.buffer,
|
||||
3: numLocations.buffer,
|
||||
};
|
||||
|
||||
final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// run inference
|
||||
interpreter.runForMultipleInputs(inputs, outputs);
|
||||
|
||||
final inferenceTimeElapsed =
|
||||
DateTime.now().millisecondsSinceEpoch - inferenceTimeStart;
|
||||
|
||||
// Maximum number of results to show
|
||||
final resultsCount = min(numResults, numLocations.getIntValue(0));
|
||||
|
||||
// Using labelOffset = 1 as ??? at index 0
|
||||
const labelOffset = 1;
|
||||
|
||||
final recognitions = <Recognition>[];
|
||||
|
||||
for (int i = 0; i < resultsCount; i++) {
|
||||
// Prediction score
|
||||
final score = outputScores.getDoubleValue(i);
|
||||
|
||||
// Label string
|
||||
final labelIndex = outputClasses.getIntValue(i) + labelOffset;
|
||||
final label = labels.elementAt(labelIndex);
|
||||
|
||||
if (score > threshold) {
|
||||
recognitions.add(
|
||||
Recognition(i, label, score),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final predictElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - predictStartTime;
|
||||
return Predictions(
|
||||
recognitions,
|
||||
Stats(
|
||||
predictElapsedTime,
|
||||
predictElapsedTime,
|
||||
inferenceTimeElapsed,
|
||||
preProcessElapsedTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:image/image.dart' as image_lib;
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/services/object_detection/models/predictions.dart';
|
||||
import 'package:photos/services/object_detection/models/recognition.dart';
|
||||
import "package:photos/services/object_detection/models/stats.dart";
|
||||
import "package:photos/services/object_detection/tflite/classifier.dart";
|
||||
import "package:tflite_flutter/tflite_flutter.dart";
|
||||
import "package:tflite_flutter_helper/tflite_flutter_helper.dart";
|
||||
|
||||
// Source: https://tfhub.dev/tensorflow/lite-model/mobilenet_v1_1.0_224/1/default/1
|
||||
class MobileNetClassifier extends Classifier {
|
||||
static final _logger = Logger("MobileNetClassifier");
|
||||
static const double threshold = 0.5;
|
||||
|
||||
@override
|
||||
String get modelPath => "models/mobilenet/mobilenet_v1_1.0_224_quant.tflite";
|
||||
|
||||
@override
|
||||
String get labelPath =>
|
||||
"assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt";
|
||||
|
||||
@override
|
||||
int get inputSize => 224;
|
||||
|
||||
@override
|
||||
Logger get logger => _logger;
|
||||
|
||||
MobileNetClassifier({
|
||||
Interpreter? interpreter,
|
||||
List<String>? labels,
|
||||
}) {
|
||||
loadModel(interpreter);
|
||||
loadLabels(labels);
|
||||
}
|
||||
|
||||
@override
|
||||
Predictions? predict(image_lib.Image image) {
|
||||
final predictStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final preProcessStart = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Create TensorImage from image
|
||||
TensorImage inputImage = TensorImage.fromImage(image);
|
||||
|
||||
// Pre-process TensorImage
|
||||
inputImage = getProcessedImage(inputImage);
|
||||
|
||||
final preProcessElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - preProcessStart;
|
||||
|
||||
// TensorBuffers for output tensors
|
||||
final output = TensorBufferUint8(outputShapes[0]);
|
||||
final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch;
|
||||
// run inference
|
||||
interpreter.run(inputImage.buffer, output.buffer);
|
||||
|
||||
final inferenceTimeElapsed =
|
||||
DateTime.now().millisecondsSinceEpoch - inferenceTimeStart;
|
||||
|
||||
final recognitions = <Recognition>[];
|
||||
for (int i = 0; i < labels.length; i++) {
|
||||
final score = output.getDoubleValue(i) / 255;
|
||||
if (score >= threshold) {
|
||||
final label = labels.elementAt(i);
|
||||
|
||||
recognitions.add(
|
||||
Recognition(i, label, score),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final predictElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - predictStartTime;
|
||||
return Predictions(
|
||||
recognitions,
|
||||
Stats(
|
||||
predictElapsedTime,
|
||||
predictElapsedTime,
|
||||
inferenceTimeElapsed,
|
||||
preProcessElapsedTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
82
lib/services/object_detection/tflite/scene_classifier.dart
Normal file
82
lib/services/object_detection/tflite/scene_classifier.dart
Normal file
|
@ -0,0 +1,82 @@
|
|||
import 'package:image/image.dart' as image_lib;
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/services/object_detection/models/predictions.dart';
|
||||
import 'package:photos/services/object_detection/models/recognition.dart';
|
||||
import "package:photos/services/object_detection/models/stats.dart";
|
||||
import "package:photos/services/object_detection/tflite/classifier.dart";
|
||||
import "package:tflite_flutter/tflite_flutter.dart";
|
||||
import "package:tflite_flutter_helper/tflite_flutter_helper.dart";
|
||||
|
||||
// Source: https://tfhub.dev/sayannath/lite-model/image-scene/1
|
||||
class SceneClassifier extends Classifier {
|
||||
static final _logger = Logger("SceneClassifier");
|
||||
static const double threshold = 0.5;
|
||||
|
||||
@override
|
||||
String get modelPath => "models/scenes/model.tflite";
|
||||
|
||||
@override
|
||||
String get labelPath => "assets/models/scenes/labels.txt";
|
||||
|
||||
@override
|
||||
int get inputSize => 224;
|
||||
|
||||
@override
|
||||
Logger get logger => _logger;
|
||||
|
||||
SceneClassifier({
|
||||
Interpreter? interpreter,
|
||||
List<String>? labels,
|
||||
}) {
|
||||
loadModel(interpreter);
|
||||
loadLabels(labels);
|
||||
}
|
||||
|
||||
@override
|
||||
Predictions? predict(image_lib.Image image) {
|
||||
final predictStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final preProcessStart = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Create TensorImage from image
|
||||
TensorImage inputImage = TensorImage.fromImage(image);
|
||||
|
||||
// Pre-process TensorImage
|
||||
inputImage = getProcessedImage(inputImage);
|
||||
final list = inputImage.getTensorBuffer().getDoubleList();
|
||||
final input = list.reshape([1, inputSize, inputSize, 3]);
|
||||
|
||||
final preProcessElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - preProcessStart;
|
||||
|
||||
final output = TensorBufferFloat(outputShapes[0]);
|
||||
|
||||
final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch;
|
||||
interpreter.run(input, output.buffer);
|
||||
final inferenceTimeElapsed =
|
||||
DateTime.now().millisecondsSinceEpoch - inferenceTimeStart;
|
||||
|
||||
final recognitions = <Recognition>[];
|
||||
for (int i = 0; i < labels.length; i++) {
|
||||
final score = output.getDoubleValue(i);
|
||||
final label = labels.elementAt(i);
|
||||
if (score >= threshold) {
|
||||
recognitions.add(
|
||||
Recognition(i, label, score),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final predictElapsedTime =
|
||||
DateTime.now().millisecondsSinceEpoch - predictStartTime;
|
||||
return Predictions(
|
||||
recognitions,
|
||||
Stats(
|
||||
predictElapsedTime,
|
||||
predictElapsedTime,
|
||||
inferenceTimeElapsed,
|
||||
preProcessElapsedTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ import "dart:typed_data";
|
|||
|
||||
import 'package:image/image.dart' as imgLib;
|
||||
import "package:photos/services/object_detection/tflite/classifier.dart";
|
||||
import 'package:photos/services/object_detection/tflite/cocossd_classifier.dart';
|
||||
import "package:photos/services/object_detection/tflite/mobilenet_classifier.dart";
|
||||
import "package:photos/services/object_detection/tflite/scene_classifier.dart";
|
||||
import 'package:tflite_flutter/tflite_flutter.dart';
|
||||
|
||||
/// Manages separate Isolate instance for inference
|
||||
|
@ -29,15 +32,32 @@ class IsolateUtils {
|
|||
sendPort.send(port.sendPort);
|
||||
|
||||
await for (final IsolateData isolateData in port) {
|
||||
final classifier = ObjectClassifier(
|
||||
interpreter: Interpreter.fromAddress(isolateData.interpreterAddress),
|
||||
labels: isolateData.labels,
|
||||
);
|
||||
final classifier = _getClassifier(isolateData);
|
||||
final image = imgLib.decodeImage(isolateData.input);
|
||||
final results = classifier.predict(image!);
|
||||
isolateData.responsePort.send(results);
|
||||
}
|
||||
}
|
||||
|
||||
static Classifier _getClassifier(IsolateData isolateData) {
|
||||
final interpreter = Interpreter.fromAddress(isolateData.interpreterAddress);
|
||||
if (isolateData.type == ClassifierType.cocossd) {
|
||||
return CocoSSDClassifier(
|
||||
interpreter: interpreter,
|
||||
labels: isolateData.labels,
|
||||
);
|
||||
} else if (isolateData.type == ClassifierType.mobilenet) {
|
||||
return MobileNetClassifier(
|
||||
interpreter: interpreter,
|
||||
labels: isolateData.labels,
|
||||
);
|
||||
} else {
|
||||
return SceneClassifier(
|
||||
interpreter: interpreter,
|
||||
labels: isolateData.labels,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundles data to pass between Isolate
|
||||
|
@ -45,11 +65,19 @@ class IsolateData {
|
|||
Uint8List input;
|
||||
int interpreterAddress;
|
||||
List<String> labels;
|
||||
ClassifierType type;
|
||||
late SendPort responsePort;
|
||||
|
||||
IsolateData(
|
||||
this.input,
|
||||
this.interpreterAddress,
|
||||
this.labels,
|
||||
this.type,
|
||||
);
|
||||
}
|
||||
|
||||
enum ClassifierType {
|
||||
cocossd,
|
||||
mobilenet,
|
||||
scenes,
|
||||
}
|
||||
|
|
|
@ -433,23 +433,36 @@ class RemoteSyncService {
|
|||
}
|
||||
|
||||
Future<int?> _getCollectionID(DeviceCollection deviceCollection) async {
|
||||
if (deviceCollection.collectionID != null) {
|
||||
if (deviceCollection.hasCollectionID()) {
|
||||
final collection =
|
||||
_collectionsService.getCollectionByID(deviceCollection.collectionID!);
|
||||
if (collection == null || collection.isDeleted) {
|
||||
_logger.warning(
|
||||
"Collection $deviceCollection.collectionID either deleted or missing "
|
||||
"for path ${deviceCollection.id}",
|
||||
);
|
||||
return null;
|
||||
if (collection != null && !collection.isDeleted) {
|
||||
return collection.id;
|
||||
}
|
||||
if (collection == null) {
|
||||
// ideally, this should never happen because the app keeps a track of
|
||||
// all collections and their IDs. But, if somehow the collection is
|
||||
// deleted, we should fetch it again
|
||||
_logger.severe(
|
||||
"Collection ${deviceCollection.collectionID} missing "
|
||||
"for pathID ${deviceCollection.id}",
|
||||
);
|
||||
_collectionsService
|
||||
.fetchCollectionByID(deviceCollection.collectionID!)
|
||||
.ignore();
|
||||
// return, by next run collection should be available.
|
||||
// we are not waiting on fetch by choice because device might have wrong
|
||||
// mapping which will result in breaking upload for other device path
|
||||
return null;
|
||||
} else if (collection.isDeleted) {
|
||||
_logger.warning("Collection ${deviceCollection.collectionID} deleted "
|
||||
"for pathID ${deviceCollection.id}, new collection will be created");
|
||||
}
|
||||
return collection.id;
|
||||
} else {
|
||||
final collection =
|
||||
await _collectionsService.getOrCreateForPath(deviceCollection.name);
|
||||
await _db.updateDeviceCollection(deviceCollection.id, collection.id);
|
||||
return collection.id;
|
||||
}
|
||||
final collection =
|
||||
await _collectionsService.getOrCreateForPath(deviceCollection.name);
|
||||
await _db.updateDeviceCollection(deviceCollection.id, collection.id);
|
||||
return collection.id;
|
||||
}
|
||||
|
||||
Future<List<File>> _getFilesToBeUploaded() async {
|
||||
|
|
|
@ -2,7 +2,6 @@ import "dart:convert";
|
|||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import 'package:photos/data/holidays.dart';
|
||||
import 'package:photos/data/months.dart';
|
||||
import 'package:photos/data/years.dart';
|
||||
|
@ -24,7 +23,6 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class SearchService {
|
||||
Future<List<File>>? _cachedFilesFuture;
|
||||
final _enteDio = NetworkClient.instance.enteDio;
|
||||
final _logger = Logger((SearchService).toString());
|
||||
final _collectionService = CollectionsService.instance;
|
||||
static const _maximumResultsLimit = 20;
|
||||
|
|
|
@ -116,10 +116,14 @@ class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> sendFeedback(BuildContext context, String feedback) async {
|
||||
Future<void> sendFeedback(
|
||||
BuildContext context,
|
||||
String feedback, {
|
||||
String type = "SubCancellation",
|
||||
}) async {
|
||||
await _dio.post(
|
||||
_config.getHttpEndpoint() + "/anonymous/feedback",
|
||||
data: {"feedback": feedback},
|
||||
data: {"feedback": feedback, "type": "type"},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -164,6 +168,10 @@ class UserService {
|
|||
final userDetails = UserDetails.fromMap(response.data);
|
||||
if (shouldCache) {
|
||||
await _preferences.setString(keyUserDetails, userDetails.toJson());
|
||||
// handle email change from different client
|
||||
if (userDetails.email != _config.getEmail()) {
|
||||
setEmail(userDetails.email);
|
||||
}
|
||||
}
|
||||
return userDetails;
|
||||
} on DioError catch (e) {
|
||||
|
@ -229,13 +237,9 @@ class UserService {
|
|||
Future<DeleteChallengeResponse?> getDeleteChallenge(
|
||||
BuildContext context,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _enteDio.get("/users/delete-challenge");
|
||||
if (response.statusCode == 200) {
|
||||
// clear data
|
||||
await dialog.hide();
|
||||
return DeleteChallengeResponse(
|
||||
allowDelete: response.data["allowDelete"] as bool,
|
||||
encryptedChallenge: response.data["encryptedChallenge"],
|
||||
|
@ -245,7 +249,6 @@ class UserService {
|
|||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
await showGenericErrorDialog(context: context);
|
||||
return null;
|
||||
}
|
||||
|
@ -253,13 +256,17 @@ class UserService {
|
|||
|
||||
Future<void> deleteAccount(
|
||||
BuildContext context,
|
||||
String challengeResponse,
|
||||
) async {
|
||||
String challengeResponse, {
|
||||
required String reasonCategory,
|
||||
required String feedback,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enteDio.delete(
|
||||
"/users/delete",
|
||||
data: {
|
||||
"challenge": challengeResponse,
|
||||
"reasonCategory": reasonCategory,
|
||||
"feedback": feedback,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import "package:dropdown_button2/dropdown_button2.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/models/delete_account.dart';
|
||||
import 'package:photos/services/local_authentication_service.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/buttons/button_widget.dart';
|
||||
|
@ -12,12 +12,32 @@ import 'package:photos/ui/components/models/button_type.dart';
|
|||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/email_util.dart';
|
||||
import "package:photos/utils/toast_util.dart";
|
||||
|
||||
class DeleteAccountPage extends StatelessWidget {
|
||||
class DeleteAccountPage extends StatefulWidget {
|
||||
const DeleteAccountPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DeleteAccountPage> createState() => _DeleteAccountPageState();
|
||||
}
|
||||
|
||||
class _DeleteAccountPageState extends State<DeleteAccountPage> {
|
||||
bool _hasConfirmedDeletion = false;
|
||||
final _feedbackTextCtrl = TextEditingController();
|
||||
final String _defaultSelection = 'Select reason';
|
||||
late String _dropdownValue = _defaultSelection;
|
||||
late final List<String> _deletionReason = [
|
||||
_defaultSelection,
|
||||
'It’s missing a key feature that I need',
|
||||
'The app or a certain feature does not \nbehave as I think it should',
|
||||
'I found another service that I like better',
|
||||
'I use a different account',
|
||||
'My reason isn’t listed',
|
||||
];
|
||||
final List<int> _reasonIndexesWhereFeedbackIsNecessary = [1, 2, 5];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
|
@ -38,68 +58,150 @@ class DeleteAccountPage extends StatelessWidget {
|
|||
child: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/broken_heart.png',
|
||||
width: 200,
|
||||
Text(
|
||||
"What is the main reason you are deleting your account?",
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
"We'll be sorry to see you go. Are you facing some issue?",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.copyWith(color: colorScheme.textMuted),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.fillFaint,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: DropdownButton2<String>(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
value: _dropdownValue,
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
_dropdownValue = newValue!;
|
||||
});
|
||||
},
|
||||
underline: const SizedBox(),
|
||||
items: _deletionReason
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
enabled: value != _defaultSelection,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
value,
|
||||
style: value != _defaultSelection
|
||||
? getEnteTextTheme(context).small
|
||||
: getEnteTextTheme(context).smallMuted,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"We are sorry to see you go. Please share your feedback to "
|
||||
"help us improve.",
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
RichText(
|
||||
// textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: const [
|
||||
TextSpan(text: "Please write to us at "),
|
||||
TextSpan(
|
||||
text: "feedback@ente.io",
|
||||
style: TextStyle(color: Color.fromRGBO(29, 185, 84, 1)),
|
||||
),
|
||||
TextSpan(
|
||||
text: ", maybe there is a way we can help.",
|
||||
),
|
||||
],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.copyWith(color: colorScheme.textMuted),
|
||||
const SizedBox(height: 4),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: colorScheme.strokeFaint, width: 1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: colorScheme.strokeFaint, width: 1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
hintText: "Feedback",
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: "Yes, send feedback",
|
||||
icon: Icons.check_outlined,
|
||||
onTap: () async {
|
||||
await sendEmail(
|
||||
context,
|
||||
to: 'feedback@ente.io',
|
||||
subject: '[Feedback]',
|
||||
);
|
||||
controller: _feedbackTextCtrl,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.tertiaryCritical,
|
||||
labelText: "No, delete account",
|
||||
icon: Icons.no_accounts_outlined,
|
||||
onTap: () async => {await _initiateDelete(context)},
|
||||
shouldSurfaceExecutionStates: false,
|
||||
)
|
||||
_shouldAskForFeedback()
|
||||
? SizedBox(
|
||||
height: 42,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
"Kindly help us with this information",
|
||||
style: getEnteTextTheme(context)
|
||||
.smallBold
|
||||
.copyWith(color: colorScheme.warning700),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(height: 42),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_hasConfirmedDeletion = !_hasConfirmedDeletion;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _hasConfirmedDeletion,
|
||||
side: CheckboxTheme.of(context).side,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_hasConfirmedDeletion = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Yes, I want to permanently delete this account and "
|
||||
"all its data.",
|
||||
style: getEnteTextTheme(context).bodyMuted,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.critical,
|
||||
labelText: "Confirm Account Deletion",
|
||||
isDisabled: _shouldBlockDeletion(),
|
||||
onTap: () async {
|
||||
await _initiateDelete(context);
|
||||
},
|
||||
shouldSurfaceExecutionStates: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: "Cancel",
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
const SafeArea(
|
||||
child: SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -107,78 +209,69 @@ class DeleteAccountPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
bool _shouldBlockDeletion() {
|
||||
return !_hasConfirmedDeletion ||
|
||||
_dropdownValue == _defaultSelection ||
|
||||
_shouldAskForFeedback();
|
||||
}
|
||||
|
||||
bool _shouldAskForFeedback() {
|
||||
return (_reasonIndexesWhereFeedbackIsNecessary
|
||||
.contains(_deletionReason.indexOf(_dropdownValue)) &&
|
||||
_feedbackTextCtrl.text.trim().isEmpty);
|
||||
}
|
||||
|
||||
Future<void> _initiateDelete(BuildContext context) async {
|
||||
final deleteChallengeResponse =
|
||||
await UserService.instance.getDeleteChallenge(context);
|
||||
if (deleteChallengeResponse == null) {
|
||||
return;
|
||||
}
|
||||
if (deleteChallengeResponse.allowDelete) {
|
||||
await _confirmAndDelete(context, deleteChallengeResponse);
|
||||
} else {
|
||||
await _requestEmailForDeletion(context);
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
title: "Confirm Account Deletion",
|
||||
body: "You are about to permanently delete your account and all its data."
|
||||
"\nThis action is irreversible.",
|
||||
firstButtonLabel: "Delete Account Permanently",
|
||||
firstButtonType: ButtonType.critical,
|
||||
firstButtonOnTap: () async {
|
||||
final deleteChallengeResponse =
|
||||
await UserService.instance.getDeleteChallenge(context);
|
||||
if (deleteChallengeResponse == null) {
|
||||
return;
|
||||
}
|
||||
if (deleteChallengeResponse.allowDelete) {
|
||||
await _delete(context, deleteChallengeResponse);
|
||||
} else {
|
||||
await _requestEmailForDeletion(context);
|
||||
}
|
||||
},
|
||||
isDismissible: false,
|
||||
);
|
||||
if (choice!.action == ButtonAction.error) {
|
||||
await showGenericErrorDialog(context: context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmAndDelete(
|
||||
Future<void> _delete(
|
||||
BuildContext context,
|
||||
DeleteChallengeResponse response,
|
||||
) async {
|
||||
final hasAuthenticated =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to initiate account deletion",
|
||||
);
|
||||
|
||||
if (hasAuthenticated) {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
title: 'Are you sure you want to delete your account?',
|
||||
body:
|
||||
'Your uploaded data will be scheduled for deletion, and your account'
|
||||
' will be permanently deleted. \n\nThis action is not reversible.',
|
||||
firstButtonLabel: "Delete my account",
|
||||
isCritical: true,
|
||||
firstButtonOnTap: () async {
|
||||
final decryptChallenge = CryptoUtil.openSealSync(
|
||||
CryptoUtil.base642bin(response.encryptedChallenge),
|
||||
CryptoUtil.base642bin(
|
||||
Configuration.instance.getKeyAttributes()!.publicKey,
|
||||
),
|
||||
Configuration.instance.getSecretKey()!,
|
||||
);
|
||||
final challengeResponseStr = utf8.decode(decryptChallenge);
|
||||
await UserService.instance
|
||||
.deleteAccount(context, challengeResponseStr);
|
||||
},
|
||||
try {
|
||||
final decryptChallenge = CryptoUtil.openSealSync(
|
||||
CryptoUtil.base642bin(response.encryptedChallenge),
|
||||
CryptoUtil.base642bin(
|
||||
Configuration.instance.getKeyAttributes()!.publicKey,
|
||||
),
|
||||
Configuration.instance.getSecretKey()!,
|
||||
);
|
||||
final challengeResponseStr = utf8.decode(decryptChallenge);
|
||||
await UserService.instance.deleteAccount(
|
||||
context,
|
||||
challengeResponseStr,
|
||||
reasonCategory: _dropdownValue,
|
||||
feedback: _feedbackTextCtrl.text.trim(),
|
||||
);
|
||||
if (choice!.action == ButtonAction.error) {
|
||||
showGenericErrorDialog(context: context);
|
||||
}
|
||||
if (choice.action != ButtonAction.first) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
await showTextInputDialog(
|
||||
context,
|
||||
title: "Your account was deleted. Would you like to leave us a note?",
|
||||
submitButtonLabel: "Send",
|
||||
hintText: "Optional, as short as you like...",
|
||||
alwaysShowSuccessState: true,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
onSubmit: (String text) async {
|
||||
// indicates user cancelled the rename request
|
||||
if (text == "" || text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await UserService.instance.sendFeedback(context, text);
|
||||
} catch (e, s) {
|
||||
Logger("Delete account").severe("Failed to send feedback", e, s);
|
||||
}
|
||||
},
|
||||
);
|
||||
showShortToast(context, "Your account has been deleted");
|
||||
} catch (e, s) {
|
||||
Logger("DeleteAccount").severe("failed to delete", e, s);
|
||||
showGenericErrorDialog(context: context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -258,7 +258,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
autofillHints: const [AutofillHints.newPassword],
|
||||
onEditingComplete: () => TextInput.finishAutofillContext(),
|
||||
decoration: InputDecoration(
|
||||
fillColor: _passwordsMatch ? _validFieldValueColor : null,
|
||||
fillColor: _passwordsMatch && _passwordIsValid
|
||||
? _validFieldValueColor
|
||||
: null,
|
||||
filled: true,
|
||||
hintText: "Confirm password",
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
|
|
|
@ -228,7 +228,6 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
|||
child: CollectionsListWidget(
|
||||
searchResults,
|
||||
widget.actionType,
|
||||
widget.showOptionToCreateNewAlbum,
|
||||
widget.selectedFiles,
|
||||
widget.sharedFiles,
|
||||
_searchQuery,
|
||||
|
|
|
@ -30,7 +30,6 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|||
class CollectionsListWidget extends StatelessWidget {
|
||||
final List<CollectionWithThumbnail> collectionsWithThumbnail;
|
||||
final CollectionActionType actionType;
|
||||
final bool showOptionToCreateNewAlbum;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final List<SharedMediaFile>? sharedFiles;
|
||||
final String searchQuery;
|
||||
|
@ -39,7 +38,6 @@ class CollectionsListWidget extends StatelessWidget {
|
|||
CollectionsListWidget(
|
||||
this.collectionsWithThumbnail,
|
||||
this.actionType,
|
||||
this.showOptionToCreateNewAlbum,
|
||||
this.selectedFiles,
|
||||
this.sharedFiles,
|
||||
this.searchQuery,
|
||||
|
@ -56,18 +54,15 @@ class CollectionsListWidget extends StatelessWidget {
|
|||
: selectedFiles?.files.length ?? 0;
|
||||
|
||||
if (collectionsWithThumbnail.isEmpty) {
|
||||
if (shouldShowCreateAlbum) {
|
||||
return _getNewAlbumWidget(context, filesCount);
|
||||
}
|
||||
return const EmptyState();
|
||||
}
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 && shouldShowCreateAlbum) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await _createNewAlbumOnTap(context, filesCount);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const NewAlbumListItemWidget(),
|
||||
);
|
||||
return _getNewAlbumWidget(context, filesCount);
|
||||
}
|
||||
final item =
|
||||
collectionsWithThumbnail[index - (shouldShowCreateAlbum ? 1 : 0)];
|
||||
|
@ -89,6 +84,16 @@ class CollectionsListWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
GestureDetector _getNewAlbumWidget(BuildContext context, int filesCount) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await _createNewAlbumOnTap(context, filesCount);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const NewAlbumListItemWidget(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createNewAlbumOnTap(
|
||||
BuildContext context,
|
||||
int filesCount,
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class DividerWithPadding extends StatelessWidget {
|
||||
final double left, top, right, bottom, thickness;
|
||||
|
||||
const DividerWithPadding({
|
||||
Key? key,
|
||||
this.left = 0,
|
||||
this.top = 0,
|
||||
this.right = 0,
|
||||
this.bottom = 0,
|
||||
this.thickness = 0.5,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(left, top, right, bottom),
|
||||
child: Divider(
|
||||
thickness: thickness,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
|
||||
class RenameDialog extends StatefulWidget {
|
||||
final String? name;
|
||||
final String type;
|
||||
final int maxLength;
|
||||
|
||||
const RenameDialog(this.name, this.type, {Key? key, this.maxLength = 100})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<RenameDialog> createState() => _RenameDialogState();
|
||||
}
|
||||
|
||||
class _RenameDialogState extends State<RenameDialog> {
|
||||
String? _newName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_newName = widget.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Enter a new name"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '${widget.type} name',
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white30,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newName = value;
|
||||
});
|
||||
},
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.text,
|
||||
initialValue: _newName,
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
"Rename",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_newName!.trim().isEmpty) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Empty name",
|
||||
"${widget.type} name cannot be empty",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_newName!.trim().length > widget.maxLength) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Name too large",
|
||||
"${widget.type} name should be less than ${widget.maxLength} characters",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(_newName!.trim());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,13 +7,13 @@ import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
|||
class InfoItemWidget extends StatelessWidget {
|
||||
final IconData leadingIcon;
|
||||
final VoidCallback? editOnTap;
|
||||
final String title;
|
||||
final String? title;
|
||||
final Future<List<Widget>> subtitleSection;
|
||||
final bool hasChipButtons;
|
||||
const InfoItemWidget({
|
||||
required this.leadingIcon,
|
||||
this.editOnTap,
|
||||
required this.title,
|
||||
this.title,
|
||||
required this.subtitleSection,
|
||||
this.hasChipButtons = false,
|
||||
super.key,
|
||||
|
@ -21,6 +21,53 @@ class InfoItemWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = <Widget>[];
|
||||
if (title != null) {
|
||||
children.addAll([
|
||||
Text(
|
||||
title!,
|
||||
style: hasChipButtons
|
||||
? getEnteTextTheme(context).smallMuted
|
||||
: getEnteTextTheme(context).body,
|
||||
),
|
||||
SizedBox(height: hasChipButtons ? 8 : 4),
|
||||
]);
|
||||
}
|
||||
|
||||
children.addAll([
|
||||
Flexible(
|
||||
child: FutureBuilder(
|
||||
future: subtitleSection,
|
||||
builder: (context, snapshot) {
|
||||
Widget child;
|
||||
if (snapshot.hasData) {
|
||||
final subtitle = snapshot.data as List<Widget>;
|
||||
if (subtitle.isNotEmpty) {
|
||||
child = Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: subtitle,
|
||||
);
|
||||
} else {
|
||||
child = const SizedBox.shrink();
|
||||
}
|
||||
} else {
|
||||
child = EnteLoadingWidget(
|
||||
padding: 3,
|
||||
size: 11,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
alignment: Alignment.centerLeft,
|
||||
);
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -39,47 +86,7 @@ class InfoItemWidget extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: hasChipButtons
|
||||
? getEnteTextTheme(context).smallMuted
|
||||
: getEnteTextTheme(context).body,
|
||||
),
|
||||
SizedBox(height: hasChipButtons ? 8 : 4),
|
||||
Flexible(
|
||||
child: FutureBuilder(
|
||||
future: subtitleSection,
|
||||
builder: (context, snapshot) {
|
||||
Widget child;
|
||||
if (snapshot.hasData) {
|
||||
final subtitle = snapshot.data as List<Widget>;
|
||||
if (subtitle.isNotEmpty) {
|
||||
child = Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: subtitle,
|
||||
);
|
||||
} else {
|
||||
child = const SizedBox.shrink();
|
||||
}
|
||||
} else {
|
||||
child = EnteLoadingWidget(
|
||||
padding: 3,
|
||||
size: 11,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
alignment: Alignment.centerLeft,
|
||||
);
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -135,9 +135,10 @@ class _ApplyCodeScreenState extends State<ApplyCodeScreen> {
|
|||
Logger('$runtimeType')
|
||||
.severe("failed to apply referral", e);
|
||||
showErrorDialogForException(
|
||||
context: context,
|
||||
exception: e as Exception,
|
||||
apiErrorPrefix: "Failed to apply code");
|
||||
context: context,
|
||||
exception: e as Exception,
|
||||
apiErrorPrefix: "Failed to apply code",
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -43,7 +43,7 @@ class _ReferralScreenState extends State<ReferralScreen> {
|
|||
await UserService.instance.getUserDetailsV2(memoryCount: false);
|
||||
final referralView =
|
||||
await StorageBonusService.instance.getGateway().getReferralView();
|
||||
return Tuple2(referralView, cachedUserDetails!);
|
||||
return Tuple2(referralView, cachedUserDetails);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -384,12 +384,6 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
|
|||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
_isFreePlanUser()
|
||||
? Text(
|
||||
"2 months free on yearly plans",
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
RepaintBoundary(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -405,10 +399,17 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
|
|||
await _filterStorePlansForUi();
|
||||
},
|
||||
),
|
||||
planText("Yearly", !showYearlyPlan)
|
||||
planText("Yearly", !showYearlyPlan),
|
||||
],
|
||||
),
|
||||
),
|
||||
_isFreePlanUser()
|
||||
? Text(
|
||||
"2 months free on yearly plans",
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/models/billing_plan.dart';
|
||||
import 'package:photos/models/subscription.dart';
|
||||
|
@ -55,6 +56,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
bool _isStripeSubscriber = false;
|
||||
bool _showYearlyPlan = false;
|
||||
EnteColorScheme colorScheme = darkScheme;
|
||||
final Logger logger = Logger("StripeSubscriptionPage");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -366,20 +368,44 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> toggleStripeSubscription(bool isRenewCancelled) async {
|
||||
// toggleStripeSubscription, based on current auto renew status, will
|
||||
// toggle the auto renew status of the user's subscription
|
||||
Future<void> toggleStripeSubscription(bool isAutoRenewDisabled) async {
|
||||
await _dialog.show();
|
||||
try {
|
||||
isRenewCancelled
|
||||
isAutoRenewDisabled
|
||||
? await _billingService.activateStripeSubscription()
|
||||
: await _billingService.cancelStripeSubscription();
|
||||
await _fetchSub();
|
||||
} catch (e) {
|
||||
showShortToast(
|
||||
context,
|
||||
isRenewCancelled ? 'Failed to renew' : 'Failed to cancel',
|
||||
isAutoRenewDisabled ? 'Failed to renew' : 'Failed to cancel',
|
||||
);
|
||||
}
|
||||
await _dialog.hide();
|
||||
if (!isAutoRenewDisabled && mounted) {
|
||||
await showTextInputDialog(
|
||||
context,
|
||||
title: "Your subscription was cancelled. Would you like to share the "
|
||||
"reason?",
|
||||
submitButtonLabel: "Send",
|
||||
hintText: "Optional, as short as you like...",
|
||||
alwaysShowSuccessState: true,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
onSubmit: (String text) async {
|
||||
// indicates user cancelled the rename request
|
||||
if (text == "" || text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await UserService.instance.sendFeedback(context, text);
|
||||
} catch (e, s) {
|
||||
logger.severe("Failed to send feedback", e, s);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _getStripePlanWidgets() {
|
||||
|
@ -492,12 +518,6 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
_isFreePlanUser()
|
||||
? Text(
|
||||
"2 months free on yearly plans",
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
RepaintBoundary(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -513,10 +533,17 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
await _filterStripeForUI();
|
||||
},
|
||||
),
|
||||
planText("Yearly", !_showYearlyPlan)
|
||||
planText("Yearly", !_showYearlyPlan),
|
||||
],
|
||||
),
|
||||
),
|
||||
_isFreePlanUser()
|
||||
? Text(
|
||||
"2 months free on yearly plans",
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -7,14 +7,12 @@ import 'package:photos/theme/ente_theme.dart';
|
|||
import 'package:photos/ui/account/change_email_dialog.dart';
|
||||
import 'package:photos/ui/account/delete_account_page.dart';
|
||||
import 'package:photos/ui/account/password_entry_page.dart';
|
||||
import 'package:photos/ui/account/recovery_key_page.dart';
|
||||
import 'package:photos/ui/components/captioned_text_widget.dart';
|
||||
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
|
||||
import "package:photos/ui/payment/subscription.dart";
|
||||
import 'package:photos/ui/settings/common_settings.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import "package:url_launcher/url_launcher_string.dart";
|
||||
|
||||
class AccountSectionWidget extends StatelessWidget {
|
||||
|
@ -35,38 +33,13 @@ class AccountSectionWidget extends StatelessWidget {
|
|||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Recovery key",
|
||||
title: "Manage subscription",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
showOnlyLoadingState: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to view your recovery key",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey = await _getOrCreateRecoveryKey(context);
|
||||
} catch (e) {
|
||||
await showGenericErrorDialog(context: context);
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
_onManageSubscriptionTapped(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
|
@ -157,7 +130,22 @@ class AccountSectionWidget extends StatelessWidget {
|
|||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
routeToPage(context, const DeleteAccountPage());
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to initiate account deletion",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
unawaited(
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const DeleteAccountPage();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
|
@ -165,12 +153,6 @@ class AccountSectionWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Future<String> _getOrCreateRecoveryKey(BuildContext context) async {
|
||||
return CryptoUtil.bin2hex(
|
||||
await UserService.instance.getOrCreateRecoveryKey(context),
|
||||
);
|
||||
}
|
||||
|
||||
void _onLogoutTapped(BuildContext context) {
|
||||
showChoiceActionSheet(
|
||||
context,
|
||||
|
@ -182,4 +164,14 @@ class AccountSectionWidget extends StatelessWidget {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onManageSubscriptionTapped(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return getSubscriptionPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:photos/ui/components/captioned_text_widget.dart';
|
|||
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
|
||||
import "package:photos/ui/growth/referral_screen.dart";
|
||||
import 'package:photos/ui/payment/subscription.dart';
|
||||
import 'package:photos/ui/settings/common_settings.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
|
||||
|
@ -26,18 +25,6 @@ class GeneralSectionWidget extends StatelessWidget {
|
|||
Widget _getSectionOptions(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Manage subscription",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_onManageSubscriptionTapped(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
|
@ -84,16 +71,6 @@ class GeneralSectionWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
void _onManageSubscriptionTapped(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return getSubscriptionPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onFamilyPlansTapped(BuildContext context) async {
|
||||
final userDetails =
|
||||
await UserService.instance.getUserDetailsV2(memoryCount: false);
|
||||
|
|
|
@ -8,12 +8,16 @@ import 'package:photos/events/two_factor_status_change_event.dart';
|
|||
import 'package:photos/services/local_authentication_service.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import "package:photos/ui/account/recovery_key_page.dart";
|
||||
import 'package:photos/ui/account/sessions_page.dart';
|
||||
import 'package:photos/ui/components/captioned_text_widget.dart';
|
||||
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
|
||||
import 'package:photos/ui/components/toggle_switch_widget.dart';
|
||||
import 'package:photos/ui/settings/common_settings.dart';
|
||||
import "package:photos/utils/crypto_util.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class SecuritySectionWidget extends StatefulWidget {
|
||||
const SecuritySectionWidget({Key? key}) : super(key: key);
|
||||
|
@ -60,6 +64,43 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
if (_config.hasConfiguredAccount()) {
|
||||
children.addAll(
|
||||
[
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Recovery key",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
showOnlyLoadingState: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to view your recovery key",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey = await _getOrCreateRecoveryKey(context);
|
||||
} catch (e) {
|
||||
await showGenericErrorDialog(context: context);
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
|
@ -186,4 +227,10 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getOrCreateRecoveryKey(BuildContext context) async {
|
||||
return CryptoUtil.bin2hex(
|
||||
await UserService.instance.getOrCreateRecoveryKey(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
|
|||
firstList.add(
|
||||
BlurMenuItemWidget(
|
||||
leadingIcon: Icons.link_outlined,
|
||||
labelText: "Create link$suffix",
|
||||
labelText: "Share link$suffix",
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null,
|
||||
),
|
||||
|
|
|
@ -15,7 +15,6 @@ class ObjectsItemWidget extends StatelessWidget {
|
|||
return InfoItemWidget(
|
||||
key: const ValueKey("Objects"),
|
||||
leadingIcon: Icons.image_search_outlined,
|
||||
title: "Objects",
|
||||
subtitleSection: _objectTags(file),
|
||||
hasChipButtons: true,
|
||||
);
|
||||
|
|
|
@ -320,6 +320,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0+3"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dropdown_button2
|
||||
sha256: "4458d81bfd24207f3d58f66f78097064e02f810f94cf1bc80bf20fe7685ebc80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
email_validator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -12,7 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.7.33+433
|
||||
version: 0.7.37+437
|
||||
|
||||
environment:
|
||||
sdk: '>=2.17.0 <3.0.0'
|
||||
|
@ -30,13 +30,14 @@ dependencies:
|
|||
collection: # dart
|
||||
computer: ^2.0.0
|
||||
confetti: ^0.6.0
|
||||
crypto: ^3.0.2
|
||||
connectivity_plus: ^3.0.3
|
||||
crypto: ^3.0.2
|
||||
cupertino_icons: ^1.0.0
|
||||
device_info: ^2.0.2
|
||||
dio: ^4.0.6
|
||||
dots_indicator: ^2.0.0
|
||||
dotted_border: ^2.0.0+2
|
||||
dropdown_button2: ^2.0.0
|
||||
email_validator: ^2.0.1
|
||||
equatable: ^2.0.5
|
||||
event_bus: ^2.0.0
|
||||
|
@ -165,7 +166,9 @@ flutter_native_splash:
|
|||
flutter:
|
||||
assets:
|
||||
- assets/
|
||||
- assets/models/
|
||||
- assets/models/cocossd/
|
||||
- assets/models/mobilenet/
|
||||
- assets/models/scenes/
|
||||
fonts:
|
||||
- family: Inter
|
||||
fonts:
|
||||
|
|
Loading…
Reference in a new issue