Merge branch 'master' into empty_albums

This commit is contained in:
Neeraj Gupta 2022-11-06 14:45:46 +05:30
commit bbbc61fd4e
No known key found for this signature in database
GPG key ID: 3C5A1684DC1729E1
118 changed files with 3632 additions and 1671 deletions

33
.github/workflows/code_quality.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Check Linter Rules
on:
pull_request:
branches:
- master
jobs:
test:
if: github.event.pull_request.draft == 'false'
name: Check the source code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v2
with:
path: ${{ runner.tool_cache }}/flutter
key: flutter-3.0.0-stable
# Setup the flutter environment.
- uses: subosito/flutter-action@v2.3.0
with:
channel: 'stable'
flutter-version: '3.0.0'
# Fetch sub modules
- run: git submodule update --init --recursive
# Get flutter dependencies.
- name: Install packages
run: flutter pub get
- name: Run Linter
run: flutter analyze --no-fatal-infos
# - name: Run Test :sed:
# run: flutter test

View file

@ -55,9 +55,11 @@ analyzer:
prefer_const_constructors: warning
prefer_const_declarations: warning
prefer_const_constructors_in_immutables: warning
prefer_final_locals: warning
unnecessary_const: error
cancel_subscriptions: error
invalid_dependency: info
use_build_context_synchronously: ignore # experimental lint, requires many changes
prefer_interpolation_to_compose_strings: ignore # later too many warnings
prefer_double_quotes: ignore # too many warnings

View file

@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 32
compileSdkVersion 33
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -47,7 +47,7 @@ android {
defaultConfig {
applicationId "io.ente.photos"
minSdkVersion 19
targetSdkVersion 30
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -1,65 +1,91 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.ente.photos">
<application android:name="${applicationName}" android:label="@string/app_name" android:icon="@mipmap/launcher_icon" android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" android:largeHeap="true">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.ente.photos">
<application android:name="${applicationName}"
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:largeHeap="true">
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<activity android:name=".MainActivity" android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:exported="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ente"/>
</intent-filter>
<!--Filter to support sharing images into our app-->
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*"/>
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2" />
<meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
<meta-data android:name="io.sentry.dsn" android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4" />
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="flutterEmbedding" android:value="2"/>
<meta-data android:name="asset_statements"
android:resource="@string/asset_statements"/>
<meta-data android:name="io.sentry.dsn"
android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4"/>
<meta-data android:name="firebase_analytics_collection_deactivated"
android:value="true"/>
</application>
<!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
<!-- https://developer.android.com/training/package-visibility/use-cases -->
<queries>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
<action android:name="android.intent.action.SENDTO"/>
<data android:scheme="mailto"/>
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"/> <!-- If you want to read images-->
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"/> <!-- If you want to read videos-->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.android.vending.BILLING"/>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -321,7 +321,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721

View file

@ -11,6 +11,7 @@ const String sentryTunnel = "https://sentry-reporter.ente.io";
const String roadmapURL = "https://roadmap.ente.io";
const int microSecondsInDay = 86400000000;
const int android11SDKINT = 30;
const int jan011991Time = 31580904000000;
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1

View file

@ -66,8 +66,10 @@ class EnteRequestInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (kDebugMode) {
assert(options.baseUrl == Network.apiEndpoint,
"interceptor should only be used for API endpoint");
assert(
options.baseUrl == Network.apiEndpoint,
"interceptor should only be used for API endpoint",
);
}
// ignore: prefer_const_constructors
options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());

View file

@ -372,8 +372,10 @@ extension DeviceFiles on FilesDB {
deviceCollections.add(deviceCollection);
}
if (includeCoverThumbnail) {
deviceCollections.sort((a, b) =>
b.thumbnail.creationTime.compareTo(a.thumbnail.creationTime));
deviceCollections.sort(
(a, b) =>
b.thumbnail.creationTime.compareTo(a.thumbnail.creationTime),
);
}
return deviceCollections;
} catch (e) {

View file

@ -12,7 +12,6 @@ import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/location.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/services/feature_flag_service.dart';
import 'package:photos/utils/file_uploader_util.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_migration/sqflite_migration.dart';
@ -611,17 +610,9 @@ class FilesDB {
}) async {
final db = await instance.database;
final order = (asc ?? false ? 'ASC' : 'DESC');
String whereClause;
List<Object> whereArgs;
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
whereClause =
'$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnMMdVisibility = ?';
whereArgs = [collectionID, startTime, endTime, visibility];
} else {
whereClause =
'$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
whereArgs = [collectionID, startTime, endTime];
}
const String whereClause =
'$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
final List<Object> whereArgs = [collectionID, startTime, endTime];
final results = await db.query(
filesTable,
@ -636,6 +627,43 @@ class FilesDB {
return FileLoadResult(files, files.length == limit);
}
Future<FileLoadResult> getFilesInCollections(
List<int> collectionIDs,
int startTime,
int endTime,
int userID, {
int limit,
bool asc,
}) async {
if (collectionIDs.isEmpty) {
return FileLoadResult(<File>[], false);
}
String inParam = "";
for (final id in collectionIDs) {
inParam += "'" + id.toString() + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final db = await instance.database;
final order = (asc ?? false ? 'ASC' : 'DESC');
final String whereClause =
'$columnCollectionID IN ($inParam) AND $columnCreationTime >= ? AND '
'$columnCreationTime <= ? AND $columnOwnerID = ?';
final List<Object> whereArgs = [startTime, endTime, userID];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
orderBy:
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
limit: limit,
);
final files = convertToFiles(results);
final dedupeResult = _deduplicatedAndFilterIgnoredFiles(files, {});
_logger.info("Fetched " + dedupeResult.length.toString() + " files");
return FileLoadResult(files, files.length == limit);
}
Future<List<File>> getFilesCreatedWithinDurations(
List<List<int>> durations,
Set<int> ignoredCollectionIDs, {
@ -1080,7 +1108,9 @@ class FilesDB {
final db = await instance.database;
final count = Sqflite.firstIntValue(
await db.rawQuery(
'SELECT COUNT(*) FROM $filesTable where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID',
'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where '
'$columnMMdVisibility'
' = $visibility AND $columnOwnerID = $ownerID',
),
);
return count;
@ -1143,25 +1173,7 @@ class FilesDB {
Future<List<File>> getLatestCollectionFiles() async {
debugPrint("Fetching latestCollectionFiles from db");
String query;
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
query = '''
SELECT $filesTable.*
FROM $filesTable
INNER JOIN
(
SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
FROM $filesTable
WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS
NOT -1 AND $columnMMdVisibility = $visibilityVisible AND
$columnUploadedFileID IS NOT -1)
GROUP BY $columnCollectionID
) latest_files
ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
''';
} else {
query = '''
const String query = '''
SELECT $filesTable.*
FROM $filesTable
INNER JOIN
@ -1173,9 +1185,7 @@ class FilesDB {
) latest_files
ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
''';
}
final db = await instance.database;
final rows = await db.rawQuery(
query,
@ -1250,6 +1260,33 @@ class FilesDB {
return result;
}
Future<Map<int, List<File>>> getAllFilesGroupByCollectionID(
List<int> ids,
) async {
final result = <int, List<File>>{};
if (ids.isEmpty) {
return result;
}
String inParam = "";
for (final id in ids) {
inParam += "'" + id.toString() + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final db = await instance.database;
final results = await db.query(
filesTable,
where: '$columnUploadedFileID IN ($inParam)',
);
final files = convertToFiles(results);
for (File eachFile in files) {
if (!result.containsKey(eachFile.collectionID)) {
result[eachFile.collectionID] = <File>[];
}
result[eachFile.collectionID].add(eachFile);
}
return result;
}
Future<Set<int>> getAllCollectionIDsOfFile(
int uploadedFileID,
) async {
@ -1276,15 +1313,28 @@ class FilesDB {
return files;
}
Future<List<File>> getAllFilesFromDB() async {
Future<List<File>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
final db = await instance.database;
final List<Map<String, dynamic>> result = await db.query(filesTable);
final List<File> files = convertToFiles(result);
final List<File> deduplicatedFiles =
_deduplicatedAndFilterIgnoredFiles(files, null);
_deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
return deduplicatedFiles;
}
Future<Map<FileType, int>> fetchFilesCountbyType(int userID) async {
final db = await instance.database;
final result = await db.rawQuery(
"SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType",
);
final filesCount = <FileType, int>{};
for (var e in result) {
filesCount.addAll({getFileType(e[columnFileType]): e.values.last});
}
return filesCount;
}
Map<String, dynamic> _getRowForFile(File file) {
final row = <String, dynamic>{};
if (file.generatedID != null) {

View file

@ -20,4 +20,6 @@ enum EventType {
deletedFromEverywhere,
archived,
unarchived,
hide,
unhide,
}

View file

@ -16,3 +16,9 @@ enum TabChangedEventSource {
collectionsPage,
backButton,
}
class TabDoubleTapEvent extends Event {
final int selectedIndex;
TabDoubleTapEvent(this.selectedIndex);
}

View file

@ -0,0 +1,57 @@
import 'package:photos/models/collection.dart';
import 'package:photos/services/file_magic_service.dart';
class CreateRequest {
String encryptedKey;
String keyDecryptionNonce;
String encryptedName;
String nameDecryptionNonce;
String type;
CollectionAttributes? attributes;
MetadataRequest? magicMetadata;
CreateRequest({
required this.encryptedKey,
required this.keyDecryptionNonce,
required this.encryptedName,
required this.nameDecryptionNonce,
required this.type,
this.attributes,
this.magicMetadata,
});
CreateRequest copyWith({
String? encryptedKey,
String? keyDecryptionNonce,
String? encryptedName,
String? nameDecryptionNonce,
String? type,
CollectionAttributes? attributes,
MetadataRequest? magicMetadata,
}) =>
CreateRequest(
encryptedKey: encryptedKey ?? this.encryptedKey,
keyDecryptionNonce: keyDecryptionNonce ?? this.keyDecryptionNonce,
encryptedName: encryptedName ?? this.encryptedName,
nameDecryptionNonce: nameDecryptionNonce ?? this.nameDecryptionNonce,
type: type ?? this.type,
attributes: attributes ?? this.attributes,
magicMetadata: magicMetadata ?? this.magicMetadata,
);
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['encryptedKey'] = encryptedKey;
map['keyDecryptionNonce'] = keyDecryptionNonce;
map['encryptedName'] = encryptedName;
map['nameDecryptionNonce'] = nameDecryptionNonce;
map['type'] = type;
if (attributes != null) {
map['attributes'] = attributes!.toMap();
}
if (magicMetadata != null) {
map['magicMetadata'] = magicMetadata!.toJson();
}
return map;
}
}

View file

@ -46,6 +46,17 @@ class Collection {
return mMdVersion > 0 && magicMetadata.visibility == visibilityArchive;
}
bool isHidden() {
if (isDefaultHidden()) {
return true;
}
return mMdVersion > 0 && (magicMetadata.visibility == visibilityHidden);
}
bool isDefaultHidden() {
return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
}
static CollectionType typeFromString(String type) {
switch (type) {
case "folder":

View file

@ -9,6 +9,7 @@ import 'package:photos/models/location.dart';
import 'package:photos/models/magic_metadata.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/services/feature_flag_service.dart';
import 'package:photos/utils/date_time_util.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/utils/exif_util.dart';
// ignore: import_of_legacy_library_into_null_safe
@ -74,16 +75,13 @@ class File extends EnteFile {
file.location = Location(asset.latitude, asset.longitude);
file.fileType = _fileTypeFromAsset(asset);
file.creationTime = asset.createDateTime.microsecondsSinceEpoch;
if (file.creationTime == 0) {
if (file.creationTime == null || (file.creationTime! <= jan011991Time)) {
try {
final parsedDateTime = DateTime.parse(
basenameWithoutExtension(file.title!)
.replaceAll("IMG_", "")
.replaceAll("VID_", "")
.replaceAll("DCIM_", "")
.replaceAll("_", " "),
);
file.creationTime = parsedDateTime.microsecondsSinceEpoch;
final parsedDateTime =
parseDateFromFileName(basenameWithoutExtension(file.title ?? ""));
file.creationTime = parsedDateTime?.microsecondsSinceEpoch ??
asset.modifiedDateTime.microsecondsSinceEpoch;
} catch (e) {
file.creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
}
@ -101,9 +99,7 @@ class File extends EnteFile {
type = FileType.image;
// PHAssetMediaSubtype.photoLive.rawValue is 8
// This hack should go away once photos_manager support livePhotos
if (asset.subtype != null &&
asset.subtype > -1 &&
(asset.subtype & 8) != 0) {
if (asset.subtype > -1 && (asset.subtype & 8) != 0) {
type = FileType.livePhoto;
}
break;
@ -165,9 +161,9 @@ class File extends EnteFile {
duration = asset.duration;
}
}
if (fileType == FileType.image) {
if (fileType == FileType.image && mediaUploadData.sourceFile != null) {
final exifTime =
await getCreationTimeFromEXIF(mediaUploadData.sourceFile);
await getCreationTimeFromEXIF(mediaUploadData.sourceFile!);
if (exifTime != null) {
creationTime = exifTime.microsecondsSinceEpoch;
}
@ -215,6 +211,10 @@ class File extends EnteFile {
}
}
String? get caption {
return pubMagicMetadata?.caption;
}
String get thumbnailUrl {
final endpoint = Configuration.instance.getHttpEndpoint();
if (endpoint != kDefaultProductionEndpoint ||

View file

@ -1,6 +1,7 @@
enum GalleryType {
homepage,
archive,
hidden,
trash,
localFolder,
// indicator for gallery view of collections shared with the user

View file

@ -1,12 +1,20 @@
import 'dart:convert';
// Visibility Constants
const visibilityVisible = 0;
const visibilityArchive = 1;
const visibilityHidden = 2;
// Collection SubType Constants
const subTypeDefaultHidden = 1;
const magicKeyVisibility = 'visibility';
// key for collection subType
const subTypeKey = 'subType';
const pubMagicKeyEditedTime = 'editedTime';
const pubMagicKeyEditedName = 'editedName';
const pubMagicKeyCaption = "caption";
class MagicMetadata {
// 0 -> visible
@ -32,8 +40,9 @@ class MagicMetadata {
class PubMagicMetadata {
int? editedTime;
String? editedName;
String? caption;
PubMagicMetadata({this.editedTime, this.editedName});
PubMagicMetadata({this.editedTime, this.editedName, this.caption});
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
PubMagicMetadata.fromJson(jsonDecode(encodedJson));
@ -46,6 +55,7 @@ class PubMagicMetadata {
return PubMagicMetadata(
editedTime: map[pubMagicKeyEditedTime],
editedName: map[pubMagicKeyEditedName],
caption: map[pubMagicKeyCaption],
);
}
}
@ -56,7 +66,19 @@ class CollectionMagicMetadata {
// 2 -> hidden etc?
int visibility;
CollectionMagicMetadata({required this.visibility});
// null/0 value -> no subType
// 1 -> DEFAULT_HIDDEN COLLECTION for files hidden individually
int? subType;
CollectionMagicMetadata({required this.visibility, this.subType});
Map<String, dynamic> toJson() {
final result = {magicKeyVisibility: visibility};
if (subType != null) {
result[subTypeKey] = subType!;
}
return result;
}
factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
@ -68,6 +90,7 @@ class CollectionMagicMetadata {
if (map == null) return null;
return CollectionMagicMetadata(
visibility: map[magicKeyVisibility] ?? visibilityVisible,
subType: map[subTypeKey],
);
}
}

View file

@ -22,5 +22,6 @@ enum ResultType {
year,
fileType,
fileExtension,
fileCaption,
event
}

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/subscription.dart';
class UserDetails extends Equatable {
@ -118,3 +119,19 @@ class FamilyData {
);
}
}
class FilesCount {
final Map<FileType, int> filesCount;
FilesCount(this.filesCount);
int get total =>
images + videos + livePhotos + (filesCount[getInt(FileType.other)] ?? 0);
int get photos => images + livePhotos;
int get images => filesCount[FileType.image] ?? 0;
int get videos => filesCount[FileType.video] ?? 0;
int get livePhotos => filesCount[FileType.livePhoto] ?? 0;
}

View file

@ -21,6 +21,7 @@ import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/api/collection/create_request.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_file_item.dart';
import 'package:photos/models/collection_items.dart';
@ -51,6 +52,7 @@ class CollectionsService {
final _localPathToCollectionID = <String, int>{};
final _collectionIDToCollections = <int, Collection>{};
final _cachedKeys = <int, Uint8List>{};
Collection cachedDefaultHiddenCollection;
CollectionsService._privateConstructor() {
_db = CollectionsDB.instance;
@ -78,6 +80,15 @@ class CollectionsService {
});
}
Configuration get config => _config;
Map<int, Collection> get collectionIDToCollections =>
_collectionIDToCollections;
FilesDB get filesDB => _filesDB;
// sync method fetches just sync the collections, not the individual files
// within the collection.
Future<List<Collection>> sync() async {
_logger.info("Syncing collections");
final lastCollectionUpdationTime =
@ -145,6 +156,22 @@ class CollectionsService {
.toSet();
}
Set<int> getHiddenCollections() {
return _collectionIDToCollections.values
.toList()
.where((element) => element.isHidden())
.map((e) => e.id)
.toSet();
}
Set<int> collectionsHiddenFromTimeline() {
return _collectionIDToCollections.values
.toList()
.where((element) => element.isHidden() || element.isArchived())
.map((e) => e.id)
.toSet();
}
int getCollectionSyncTime(int collectionID) {
return _prefs
.getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
@ -177,6 +204,8 @@ class CollectionsService {
}) async {
final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
final usersCollection = getActiveCollections();
// remove any hidden collection to avoid accidental rendering on UI
usersCollection.removeWhere((element) => element.isHidden());
if (!includedOwnedByOthers) {
final userID = Configuration.instance.getUserID();
usersCollection.removeWhere((c) => c.owner.id != userID);
@ -298,6 +327,7 @@ class CollectionsService {
}
Uint8List _getDecryptedKey(Collection collection) {
debugPrint("Finding collection decryption key for ${collection.id}");
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
if (collection.owner.id == _config.getUserID()) {
if (_config.getKey() == null) {
@ -820,17 +850,17 @@ class CollectionsService {
List<File> files,
) {
if (toCollectionID == fromCollectionID) {
throw AssertionError("can't move to same album");
throw AssertionError("Can't move to same album");
}
for (final file in files) {
if (file.uploadedFileID == null) {
throw AssertionError("can only move uploaded memories");
throw AssertionError("Can only move uploaded memories");
}
if (file.collectionID != fromCollectionID) {
throw AssertionError("all memories should belong to the same album");
throw AssertionError("All memories should belong to the same album");
}
if (file.ownerID != Configuration.instance.getUserID()) {
throw AssertionError("can only move memories uploaded by you");
throw AssertionError("Can only move memories uploaded by you");
}
}
}
@ -854,11 +884,16 @@ class CollectionsService {
RemoteSyncService.instance.sync(silently: true);
}
Future<Collection> createAndCacheCollection(Collection collection) async {
Future<Collection> createAndCacheCollection(
Collection collection, {
CreateRequest createRequest,
}) async {
final dynamic payload =
createRequest != null ? createRequest.toJson() : collection.toMap();
return _enteDio
.post(
"/collections",
data: collection.toMap(),
data: payload,
)
.then((response) {
final collection = Collection.fromMap(response.data["collection"]);

View file

@ -73,10 +73,8 @@ class FeatureFlagService {
.getDio()
.get("https://static.ente.io/feature_flags.json");
final flagsResponse = FeatureFlags.fromMap(response.data);
if (flagsResponse != null) {
_prefs.setString(_featureFlagsKey, flagsResponse.toJson());
_featureFlags = flagsResponse;
}
_prefs.setString(_featureFlagsKey, flagsResponse.toJson());
_featureFlags = flagsResponse;
} catch (e) {
_logger.severe("Failed to sync feature flags ", e);
}

View file

@ -0,0 +1,139 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/api/collection/create_request.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/file_magic_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/dialog_util.dart';
extension HiddenService on CollectionsService {
static final _logger = Logger("HiddenCollectionService");
// getDefaultHiddenCollection will return null if there's no default
// collection
Future<Collection> getDefaultHiddenCollection() async {
if (cachedDefaultHiddenCollection != null) {
return cachedDefaultHiddenCollection;
}
final int userID = config.getUserID()!;
final Collection? defaultHidden =
collectionIDToCollections.values.firstWhereOrNull(
(element) => element.isDefaultHidden() && element.owner!.id == userID,
);
if (defaultHidden != null) {
cachedDefaultHiddenCollection = defaultHidden;
return cachedDefaultHiddenCollection;
}
final Collection createdHiddenCollection =
await _createDefaultHiddenAlbum();
cachedDefaultHiddenCollection = createdHiddenCollection;
return cachedDefaultHiddenCollection;
}
Future<bool> hideFiles(
BuildContext context,
List<File> filesToHide, {
bool forceHide = false,
}) async {
final int userID = config.getUserID()!;
final List<int> uploadedIDs = <int>[];
final dialog = createProgressDialog(
context,
"Hiding...",
);
await dialog.show();
try {
for (File file in filesToHide) {
if (file.uploadedFileID == null) {
throw AssertionError("Can only hide uploaded files");
}
if (file.ownerID != userID) {
throw AssertionError("Can only hide files owned by user");
}
uploadedIDs.add(file.uploadedFileID!);
}
final defaultHiddenCollection = await getDefaultHiddenCollection();
final Map<int, List<File>> collectionToFilesMap =
await filesDB.getAllFilesGroupByCollectionID(uploadedIDs);
for (MapEntry<int, List<File>> entry in collectionToFilesMap.entries) {
if (entry.key == defaultHiddenCollection.id) {
_logger.finest('file already part of hidden collection');
continue;
}
await move(defaultHiddenCollection.id, entry.key, entry.value);
}
Bus.instance.fire(ForceReloadHomeGalleryEvent());
Bus.instance.fire(
LocalPhotosUpdatedEvent(filesToHide, type: EventType.unarchived),
);
await dialog.hide();
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, "Oops", e.message as String);
} catch (e, s) {
_logger.severe("Could not hide", e, s);
await dialog.hide();
showGenericErrorDialog(context);
return false;
} finally {
await dialog.hide();
}
return true;
}
Future<Collection> _createDefaultHiddenAlbum() async {
final key = CryptoUtil.generateKey();
final encryptedKeyData = CryptoUtil.encryptSync(key, config.getKey()!);
final encryptedName = CryptoUtil.encryptSync(
utf8.encode(".Hidden") as Uint8List,
key,
);
final jsonToUpdate = CollectionMagicMetadata(
visibility: visibilityHidden,
subType: subTypeDefaultHidden,
).toJson();
assert(jsonToUpdate.length == 2, "metadata should have two keys");
final encryptedMMd = await CryptoUtil.encryptChaCha(
utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
key,
);
final MetadataRequest metadataRequest = MetadataRequest(
version: 1,
count: jsonToUpdate.length,
data: Sodium.bin2base64(encryptedMMd.encryptedData!),
header: Sodium.bin2base64(encryptedMMd.header!),
);
final CreateRequest createRequest = CreateRequest(
encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
encryptedName: Sodium.bin2base64(encryptedName.encryptedData!),
nameDecryptionNonce: Sodium.bin2base64(encryptedName.nonce!),
type: CollectionType.album.toString(),
attributes: CollectionAttributes(),
magicMetadata: metadataRequest,
);
_logger.info("Creating Hidden Collection");
final collection =
await createAndCacheCollection(null, createRequest: createRequest);
_logger.info("Creating Hidden Collection Created Successfully");
final Collection collectionFromServer =
await fetchCollectionByID(collection.id);
_logger.info("Fetched Created Hidden Collection Successfully");
return collectionFromServer;
}
}

View file

@ -264,8 +264,10 @@ Future<List<AssetEntity>> _getAllAssetLists(AssetPathEntity pathEntity) async {
size: assetFetchPageSize,
);
Bus.instance.fire(
LocalImportProgressEvent(pathEntity.name,
currentPage * assetFetchPageSize + currentPageResult.length),
LocalImportProgressEvent(
pathEntity.name,
currentPage * assetFetchPageSize + currentPageResult.length,
),
);
result.addAll(currentPageResult);
currentPage = currentPage + 1;

View file

@ -74,11 +74,11 @@ class MemoriesService extends ChangeNotifier {
date.add(const Duration(days: daysAfter)).microsecondsSinceEpoch;
durations.add([startCreationTime, endCreationTime]);
}
final archivedCollectionIds =
CollectionsService.instance.getArchivedCollections();
final ignoredCollections =
CollectionsService.instance.collectionsHiddenFromTimeline();
final files = await _filesDB.getFilesCreatedWithinDurations(
durations,
archivedCollectionIds,
ignoredCollections,
);
final seenTimes = await _memoriesDB.getSeenTimes();
final List<Memory> memories = [];

View file

@ -129,6 +129,7 @@ class RemoteSyncService {
// session are not processed now
sync();
} else {
debugPrint("Fire backup completed event");
Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
}
} else {
@ -259,7 +260,6 @@ class RemoteSyncService {
await _db.getDevicePathIDToLocalIDMap();
bool moreFilesMarkedForBackup = false;
for (final deviceCollection in deviceCollections) {
_logger.fine("processing ${deviceCollection.name}");
final Set<String> localIDsToSync =
pathIdToLocalIDs[deviceCollection.id] ?? {};
if (deviceCollection.uploadStrategy == UploadStrategy.ifMissing) {
@ -360,16 +360,20 @@ class RemoteSyncService {
if (pendingUploads.isEmpty) {
continue;
} else {
_logger.info("RemovingFiles $collectionIDs: pendingUploads "
"${pendingUploads.length}");
_logger.info(
"RemovingFiles $collectionIDs: pendingUploads "
"${pendingUploads.length}",
);
}
final Set<String> localIDsInOtherFileEntries =
await _db.getLocalIDsPresentInEntries(
pendingUploads,
collectionID,
);
_logger.info("RemovingFiles $collectionIDs: filesInOtherCollection "
"${localIDsInOtherFileEntries.length}");
_logger.info(
"RemovingFiles $collectionIDs: filesInOtherCollection "
"${localIDsInOtherFileEntries.length}",
);
final List<File> entriesToUpdate = [];
final List<int> entriesToDelete = [];
for (File pendingUpload in pendingUploads) {
@ -400,7 +404,7 @@ class RemoteSyncService {
if (collectionByID == null || collectionByID.isDeleted) {
_logger.info(
"Collection $deviceCollectionID either deleted or missing "
"for path ${deviceCollection.name}",
"for path ${deviceCollection.id}",
);
deviceCollectionID = -1;
}

View file

@ -32,28 +32,23 @@ class SearchService {
static final SearchService instance = SearchService._privateConstructor();
Future<void> init() async {
// Intention of delay is to give more CPU cycles to other tasks
// 8 is just a magic number
Future.delayed(const Duration(seconds: 8), () async {
/* In case home screen loads before 8 seconds and user starts search,
future will not be null.So here getAllFiles won't run again in that case. */
if (_cachedFilesFuture == null) {
_getAllFiles();
}
});
Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
// only invalidate, let the load happen on demand
_cachedFilesFuture = null;
});
}
Set<int> ignoreCollections() {
return CollectionsService.instance.getHiddenCollections();
}
Future<List<File>> _getAllFiles() async {
if (_cachedFilesFuture != null) {
return _cachedFilesFuture;
}
_logger.fine("Reading all files from db");
_cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
_cachedFilesFuture =
FilesDB.instance.getAllFilesFromDB(ignoreCollections());
return _cachedFilesFuture;
}
@ -133,7 +128,11 @@ class SearchService {
if (collectionSearchResults.length >= _maximumResultsLimit) {
break;
}
if (c.collection.name.toLowerCase().contains(query.toLowerCase())) {
if (!c.collection.isHidden() &&
c.collection.name.toLowerCase().contains(
query.toLowerCase(),
)) {
collectionSearchResults.add(AlbumSearchResult(c));
}
}
@ -172,7 +171,7 @@ class SearchService {
final matchedFiles =
await FilesDB.instance.getFilesCreatedWithinDurations(
_getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
null,
ignoreCollections(),
order: 'DESC',
);
if (matchedFiles.isNotEmpty) {
@ -209,6 +208,30 @@ class SearchService {
return searchResults;
}
Future<List<GenericSearchResult>> getCaptionResults(
String query,
) async {
final List<GenericSearchResult> searchResults = [];
if (query.isEmpty) {
return searchResults;
}
final RegExp pattern = RegExp(query, caseSensitive: false);
final List<File> allFiles = await _getAllFiles();
final matchedFiles = allFiles
.where((e) => e.caption != null && pattern.hasMatch(e.caption))
.toList();
if (matchedFiles.isNotEmpty) {
searchResults.add(
GenericSearchResult(
ResultType.fileCaption,
query,
matchedFiles,
),
);
}
return searchResults;
}
Future<List<GenericSearchResult>> getFileExtensionResults(
String query,
) async {
@ -248,7 +271,7 @@ class SearchService {
final matchedFiles =
await FilesDB.instance.getFilesCreatedWithinDurations(
_getDurationsOfMonthInEveryYear(month.monthNumber),
null,
ignoreCollections(),
order: 'DESC',
);
if (matchedFiles.isNotEmpty) {
@ -277,7 +300,7 @@ class SearchService {
final matchedFiles =
await FilesDB.instance.getFilesCreatedWithinDurations(
_getDurationsForCalendarDateInEveryYear(day, month, year: year),
null,
ignoreCollections(),
order: 'DESC',
);
if (matchedFiles.isNotEmpty) {
@ -305,7 +328,7 @@ class SearchService {
Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
return await FilesDB.instance.getFilesCreatedWithinDurations(
[durationOfYear],
null,
ignoreCollections(),
order: "DESC",
);
}

View file

@ -11,11 +11,13 @@ class EnteColorScheme {
// Backdrop Colors
final Color backdropBase;
final Color backdropBaseMute;
final Color backdropFaint;
// Text Colors
final Color textBase;
final Color textMuted;
final Color textFaint;
final Color blurTextBase;
// Fill Colors
final Color fillBase;
@ -27,6 +29,9 @@ class EnteColorScheme {
final Color strokeMuted;
final Color strokeFaint;
final Color strokeFainter;
final Color blurStrokeBase;
final Color blurStrokeFaint;
final Color blurStrokePressed;
// Fixed Colors
final Color primary700;
@ -49,9 +54,11 @@ class EnteColorScheme {
this.backgroundElevated2,
this.backdropBase,
this.backdropBaseMute,
this.backdropFaint,
this.textBase,
this.textMuted,
this.textFaint,
this.blurTextBase,
this.fillBase,
this.fillMuted,
this.fillFaint,
@ -59,6 +66,9 @@ class EnteColorScheme {
this.strokeMuted,
this.strokeFaint,
this.strokeFainter,
this.blurStrokeBase,
this.blurStrokeFaint,
this.blurStrokePressed,
this.tabIcon, {
this.primary700 = _primary700,
this.primary500 = _primary500,
@ -76,10 +86,12 @@ const EnteColorScheme lightScheme = EnteColorScheme(
backgroundElevatedLight,
backgroundElevated2Light,
backdropBaseLight,
backdropBaseMuteLight,
backdropMutedLight,
backdropFaintLight,
textBaseLight,
textMutedLight,
textFaintLight,
blurTextBaseLight,
fillBaseLight,
fillMutedLight,
fillFaintLight,
@ -87,6 +99,9 @@ const EnteColorScheme lightScheme = EnteColorScheme(
strokeMutedLight,
strokeFaintLight,
strokeFainterLight,
blurStrokeBaseLight,
blurStrokeFaintLight,
blurStrokePressedLight,
tabIconLight,
);
@ -95,10 +110,12 @@ const EnteColorScheme darkScheme = EnteColorScheme(
backgroundElevatedDark,
backgroundElevated2Dark,
backdropBaseDark,
backdropBaseMuteDark,
backdropMutedDark,
backdropFaintDark,
textBaseDark,
textMutedDark,
textFaintDark,
blurTextBaseDark,
fillBaseDark,
fillMutedDark,
fillFaintDark,
@ -106,6 +123,9 @@ const EnteColorScheme darkScheme = EnteColorScheme(
strokeMutedDark,
strokeFaintDark,
strokeFainterDark,
blurStrokeBaseDark,
blurStrokeFaintDark,
blurStrokePressedDark,
tabIconDark,
);
@ -120,19 +140,23 @@ const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1);
// Backdrop Colors
const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.75);
const Color backdropBaseMuteLight = Color.fromRGBO(255, 255, 255, 0.30);
const Color backdropMutedLight = Color.fromRGBO(255, 255, 255, 0.30);
const Color backdropFaintLight = Color.fromRGBO(255, 255, 255, 0.15);
const Color backdropBaseDark = Color.fromRGBO(0, 0, 0, 0.65);
const Color backdropBaseMuteDark = Color.fromRGBO(0, 0, 0, 0.20);
const Color backdropMutedDark = Color.fromRGBO(0, 0, 0, 0.20);
const Color backdropFaintDark = Color.fromRGBO(0, 0, 0, 0.08);
// Text Colors
const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
const Color textMutedLight = Color.fromRGBO(0, 0, 0, 0.6);
const Color textFaintLight = Color.fromRGBO(0, 0, 0, 0.5);
const Color blurTextBaseLight = Color.fromRGBO(0, 0, 0, 0.65);
const Color textBaseDark = Color.fromRGBO(255, 255, 255, 1);
const Color textMutedDark = Color.fromRGBO(255, 255, 255, 0.7);
const Color textFaintDark = Color.fromRGBO(255, 255, 255, 0.5);
const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
// Fill Colors
const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
@ -148,11 +172,17 @@ const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);
const Color strokeMutedLight = Color.fromRGBO(0, 0, 0, 0.24);
const Color strokeFaintLight = Color.fromRGBO(0, 0, 0, 0.12);
const Color strokeFainterLight = Color.fromRGBO(0, 0, 0, 0.06);
const Color blurStrokeBaseLight = Color.fromRGBO(0, 0, 0, 0.65);
const Color blurStrokeFaintLight = Color.fromRGBO(0, 0, 0, 0.08);
const Color blurStrokePressedLight = Color.fromRGBO(0, 0, 0, 0.50);
const Color strokeBaseDark = Color.fromRGBO(255, 255, 255, 1);
const Color strokeMutedDark = Color.fromRGBO(255, 255, 255, 0.24);
const Color strokeFaintDark = Color.fromRGBO(255, 255, 255, 0.16);
const Color strokeFainterDark = Color.fromRGBO(255, 255, 255, 0.08);
const Color blurStrokeBaseDark = Color.fromRGBO(0, 0, 0, 0.90);
const Color blurStrokeFaintDark = Color.fromRGBO(0, 0, 0, 0.08);
const Color blurStrokePressedDark = Color.fromRGBO(0, 0, 0, 0.50);
// Other colors
const Color tabIconLight = Color.fromRGBO(0, 0, 0, 0.85);

View file

@ -5,6 +5,18 @@ const FontWeight _regularWeight = FontWeight.w500;
const FontWeight _boldWeight = FontWeight.w600;
const String _fontFamily = 'Inter';
const TextStyle brandStyleSmall = TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
fontSize: 21,
);
const TextStyle brandStyleMedium = TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
fontSize: 24,
);
const TextStyle h1 = TextStyle(
fontSize: 48,
height: 48 / 28,
@ -31,7 +43,7 @@ const TextStyle large = TextStyle(
);
const TextStyle body = TextStyle(
fontSize: 16,
height: 19.4 / 16.0,
height: 20 / 16.0,
fontWeight: _regularWeight,
fontFamily: _fontFamily,
);
@ -71,6 +83,8 @@ class EnteTextTheme {
final TextStyle miniBold;
final TextStyle tiny;
final TextStyle tinyBold;
final TextStyle brandSmall;
final TextStyle brandMedium;
const EnteTextTheme({
required this.h1,
@ -89,6 +103,8 @@ class EnteTextTheme {
required this.miniBold,
required this.tiny,
required this.tinyBold,
required this.brandSmall,
required this.brandMedium,
});
}
@ -113,5 +129,7 @@ EnteTextTheme _buildEnteTextStyle(Color color) {
miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
tiny: tiny.copyWith(color: color),
tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
brandSmall: brandStyleSmall.copyWith(color: color),
brandMedium: brandStyleMedium.copyWith(color: color),
);
}

View file

@ -63,142 +63,148 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
: 120;
return Scaffold(
appBar: widget.showProgressBar
? AppBar(
elevation: 0,
title: Hero(
tag: "recovery_key",
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 3,
selectedColor:
Theme.of(context).colorScheme.greenAlternative,
roundedEdges: const Radius.circular(10),
unselectedColor: Theme.of(context)
.colorScheme
.stepProgressUnselectedColor,
),
appBar: widget.showProgressBar
? AppBar(
elevation: 0,
title: Hero(
tag: "recovery_key",
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 3,
selectedColor: Theme.of(context).colorScheme.greenAlternative,
roundedEdges: const Radius.circular(10),
unselectedColor:
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
)
: widget.showAppBar
? AppBar(
elevation: 0,
title: Text(widget.title ?? "Recovery key"),
)
: null,
body: Padding(
padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
widget.showAppBar
? const SizedBox.shrink()
: Text(
widget.title ?? "Recovery key",
style: Theme.of(context).textTheme.headline4,
),
Padding(
padding:
EdgeInsets.all(widget.showAppBar ? 0 : 12)),
Text(
widget.text ??
"If you forget your password, the only way you can recover your data is with this key.",
style: Theme.of(context).textTheme.subtitle1,
),
const Padding(padding: EdgeInsets.only(top: 24)),
DottedBorder(
color: const Color.fromRGBO(17, 127, 56, 1),
//color of dotted/dash line
strokeWidth: 1,
//thickness of dash/dots
dashPattern: const [6, 6],
radius: const Radius.circular(8),
//dash patterns, 10 is dash width, 6 is space width
child: SizedBox(
//inner container
// height: 120, //height of inner container
width: double
.infinity, //width to 100% match to parent container.
// ignore: prefer_const_literals_to_create_immutables
child: Column(
children: [
GestureDetector(
onTap: () async {
await Clipboard.setData(
ClipboardData(text: recoveryKey),
);
showToast(context,
"Recovery key copied to clipboard");
setState(() {
_hasTriedToSave = true;
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color.fromRGBO(
49, 155, 86, .2),
),
)
: widget.showAppBar
? AppBar(
elevation: 0,
title: Text(widget.title ?? "Recovery key"),
)
: null,
body: Padding(
padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
widget.showAppBar
? const SizedBox.shrink()
: Text(
widget.title ?? "Recovery key",
style: Theme.of(context).textTheme.headline4,
),
Padding(
padding: EdgeInsets.all(widget.showAppBar ? 0 : 12),
),
Text(
widget.text ??
"If you forget your password, the only way you can recover your data is with this key.",
style: Theme.of(context).textTheme.subtitle1,
),
const Padding(padding: EdgeInsets.only(top: 24)),
DottedBorder(
color: const Color.fromRGBO(17, 127, 56, 1),
//color of dotted/dash line
strokeWidth: 1,
//thickness of dash/dots
dashPattern: const [6, 6],
radius: const Radius.circular(8),
//dash patterns, 10 is dash width, 6 is space width
child: SizedBox(
//inner container
// height: 120, //height of inner container
width: double
.infinity, //width to 100% match to parent container.
// ignore: prefer_const_literals_to_create_immutables
child: Column(
children: [
GestureDetector(
onTap: () async {
await Clipboard.setData(
ClipboardData(text: recoveryKey),
);
showToast(
context,
"Recovery key copied to clipboard",
);
setState(() {
_hasTriedToSave = true;
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color.fromRGBO(
49,
155,
86,
.2,
),
borderRadius: const BorderRadius.all(
Radius.circular(2),
),
color: Theme.of(context)
.colorScheme
.recoveryKeyBoxColor,
),
padding: const EdgeInsets.all(20),
width: double.infinity,
child: Text(
recoveryKey,
style:
Theme.of(context).textTheme.bodyText1,
borderRadius: const BorderRadius.all(
Radius.circular(2),
),
color: Theme.of(context)
.colorScheme
.recoveryKeyBoxColor,
),
padding: const EdgeInsets.all(20),
width: double.infinity,
child: Text(
recoveryKey,
style:
Theme.of(context).textTheme.bodyText1,
),
),
],
),
),
],
),
),
SizedBox(
height: 80,
),
SizedBox(
height: 80,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
widget.subText ??
"We dont store this key, please save this in a safe place.",
style: Theme.of(context).textTheme.bodyText1,
),
),
),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
widget.subText ??
"We dont store this key, please save this in a safe place.",
style: Theme.of(context).textTheme.bodyText1,
),
padding: const EdgeInsets.fromLTRB(10, 10, 10, 42),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _saveOptions(context, recoveryKey),
),
),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 10, 10, 42),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _saveOptions(context, recoveryKey),
),
),
)
],
), // columnEnds
),
)
],
), // columnEnds
),
);
},
),
));
),
);
},
),
),
);
}
List<Widget> _saveOptions(BuildContext context, String recoveryKey) {

View file

@ -148,16 +148,14 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
SizedBox(
width: double.infinity,
child: Text(
'Verify recovery key',
'Confirm recovery key',
style: enteTheme.textTheme.h3Bold,
textAlign: TextAlign.left,
),
),
const SizedBox(height: 18),
Text(
"If you forget your password, your recovery key is the "
"only way to recover your photos.\n\nPlease verify that "
"you have safely backed up your 24 word recovery key by re-entering it.",
"Your recovery key is the only way to recover your photos if you forget your password. You can find your recovery key in Settings > Account.\n\nPlease enter your recovery key here to verify that you have saved it correctly.",
style: enteTheme.textTheme.small
.copyWith(color: enteTheme.colorScheme.textMuted),
),
@ -187,12 +185,6 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
},
),
const SizedBox(height: 12),
Text(
"If you saved the recovery key from older app versions, you might have a 64 character recovery code instead of 24 words. You can enter that too.",
style: enteTheme.textTheme.mini
.copyWith(color: enteTheme.colorScheme.textMuted),
),
const SizedBox(height: 8),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
@ -204,8 +196,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
children: [
GradientButton(
onTap: _verifyRecoveryKey,
text: "Verify",
iconData: Icons.shield_outlined,
text: "Confirm",
),
const SizedBox(height: 8),
],

View file

@ -0,0 +1,143 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/divider_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/components/menu_section_description_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import 'package:photos/ui/components/toggle_switch_widget.dart';
class BackupSettingsScreen extends StatelessWidget {
const BackupSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: const TitleBarTitleWidget(
title: "Backup settings",
),
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backup over mobile data",
),
menuItemColor: colorScheme.fillFaint,
trailingSwitch: ToggleSwitchWidget(
value: () {
return Configuration.instance
.shouldBackupOverMobileData();
},
onChanged: () async {
await Configuration.instance
.setBackupOverMobileData(
!Configuration.instance
.shouldBackupOverMobileData(),
);
},
),
borderRadius: 8,
alignCaptionedTextToLeft: true,
isBottomBorderRadiusRemoved: true,
isGestureDetectorDisabled: true,
),
DividerWidget(
dividerType: DividerType.menuNoIcon,
bgColor: colorScheme.fillFaint,
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backup videos",
),
menuItemColor: colorScheme.fillFaint,
trailingSwitch: ToggleSwitchWidget(
value: () =>
Configuration.instance.shouldBackupVideos(),
onChanged: () => Configuration.instance
.setShouldBackupVideos(
!Configuration.instance.shouldBackupVideos(),
),
),
borderRadius: 8,
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
isGestureDetectorDisabled: true,
),
],
),
const SizedBox(height: 24),
Platform.isIOS
? Column(
children: [
MenuItemWidget(
captionedTextWidget:
const CaptionedTextWidget(
title: "Disable auto lock",
),
menuItemColor: colorScheme.fillFaint,
trailingSwitch: ToggleSwitchWidget(
value: () => Configuration.instance
.shouldKeepDeviceAwake(),
onChanged: () {
return Configuration.instance
.setShouldKeepDeviceAwake(
!Configuration.instance
.shouldKeepDeviceAwake(),
);
},
),
borderRadius: 8,
alignCaptionedTextToLeft: true,
isGestureDetectorDisabled: true,
),
const MenuSectionDescriptionWidget(
content:
"Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster.",
)
],
)
: const SizedBox.shrink(),
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}

View file

@ -0,0 +1,104 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/ui/viewer/gallery/archive_page.dart';
import 'package:photos/utils/navigation_util.dart';
class ArchivedCollectionsButtonWidget extends StatelessWidget {
final TextStyle textStyle;
const ArchivedCollectionsButtonWidget(
this.textStyle, {
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(0),
side: BorderSide(
width: 0.5,
color: Theme.of(context).iconTheme.color.withOpacity(0.24),
),
),
child: SizedBox(
height: 48,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.archive_outlined,
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
future: FilesDB.instance.fileCountWithVisibility(
visibilityArchive,
Configuration.instance.getUserID(),
),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data > 0) {
return RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Archive",
style: Theme.of(context).textTheme.subtitle1,
),
const TextSpan(text: " \u2022 "),
TextSpan(
text: snapshot.data.toString(),
),
//need to query in db and bring this value
],
),
);
} else {
return RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Archive",
style: Theme.of(context).textTheme.subtitle1,
),
//need to query in db and bring this value
],
),
);
}
},
),
],
),
Icon(
Icons.chevron_right,
color: Theme.of(context).iconTheme.color,
),
],
),
),
),
onPressed: () async {
routeToPage(
context,
ArchivePage(),
);
},
);
}
}

View file

@ -55,7 +55,7 @@ class CollectionItem extends StatelessWidget {
FutureBuilder<int>(
future: FilesDB.instance.collectionFileCount(c.collection.id),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data! > 0) {
if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: enteTextTheme.small.copyWith(

View file

@ -1,49 +0,0 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
class EnteSectionTitle extends StatelessWidget {
final double opacity;
const EnteSectionTitle({
this.opacity = 0.8,
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: "On ",
style: Theme.of(context)
.textTheme
.headline6
.copyWith(fontSize: 22),
),
TextSpan(
text: "ente",
style: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
fontSize: 22,
color: Theme.of(context).colorScheme.defaultTextColor,
),
),
],
),
),
),
],
),
);
}
}

View file

@ -1,10 +1,8 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/ui/viewer/gallery/archive_page.dart';
import 'package:photos/services/local_authentication_service.dart';
import 'package:photos/ui/viewer/gallery/hidden_page.dart';
import 'package:photos/utils/navigation_util.dart';
class HiddenCollectionsButtonWidget extends StatelessWidget {
@ -44,44 +42,25 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
future: FilesDB.instance.fileCountWithVisibility(
visibilityArchive,
Configuration.instance.getUserID(),
RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Hidden",
style: Theme.of(context).textTheme.subtitle1,
),
const TextSpan(text: " \u2022 "),
WidgetSpan(
child: Icon(
Icons.lock_outline,
size: 16,
color: Theme.of(context).iconTheme.color,
),
),
//need to query in db and bring this value
],
),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data > 0) {
return RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Hidden",
style: Theme.of(context).textTheme.subtitle1,
),
const TextSpan(text: " \u2022 "),
TextSpan(
text: snapshot.data.toString(),
),
//need to query in db and bring this value
],
),
);
} else {
return RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Hidden",
style: Theme.of(context).textTheme.subtitle1,
),
//need to query in db and bring this value
],
),
);
}
},
),
],
),
@ -94,10 +73,17 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
),
),
onPressed: () async {
routeToPage(
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
context,
ArchivePage(),
"Please authenticate to view your hidden files",
);
if (hasAuthenticated) {
routeToPage(
context,
HiddenPage(),
);
}
},
);
}

View file

@ -1,35 +1,61 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/theme/text_style.dart';
class SectionTitle extends StatelessWidget {
final String title;
final Alignment alignment;
final double opacity;
final String? title;
final RichText? titleWithBrand;
const SectionTitle(
this.title, {
this.opacity = 0.8,
Key key,
this.alignment = Alignment.centerLeft,
const SectionTitle({
this.title,
this.titleWithBrand,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final enteTextTheme = getEnteTextTheme(context);
Widget child;
if (titleWithBrand != null) {
child = titleWithBrand!;
} else if (title != null) {
child = Text(
title!,
style: enteTextTheme.largeBold,
);
} else {
child = const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
child: Column(
children: [
Align(
alignment: alignment,
child: Text(
title,
style:
Theme.of(context).textTheme.headline6.copyWith(fontSize: 22),
),
alignment: Alignment.centerLeft,
child: child,
),
],
),
);
}
}
RichText getOnEnteSection(BuildContext context) {
final EnteTextTheme textTheme = getEnteTextTheme(context);
return RichText(
text: TextSpan(
children: [
TextSpan(
text: "On ",
style: TextStyle(
fontWeight: FontWeight.w600,
fontFamily: 'Inter',
fontSize: 21,
color: textTheme.brandSmall.color,
),
),
TextSpan(text: "ente", style: textTheme.brandSmall),
],
),
);
}

View file

@ -14,8 +14,8 @@ import 'package:photos/events/user_logged_out_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/ui/collections/archived_collections_button_widget.dart';
import 'package:photos/ui/collections/device_folders_grid_view_widget.dart';
import 'package:photos/ui/collections/ente_section_title.dart';
import 'package:photos/ui/collections/hidden_collections_button_widget.dart';
import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
import 'package:photos/ui/collections/section_title.dart';
@ -124,7 +124,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
child: Column(
children: [
const SizedBox(height: 12),
const SectionTitle("On device"),
const SectionTitle(title: "On device"),
const SizedBox(height: 12),
const DeviceFoldersGridViewWidget(),
const Padding(padding: EdgeInsets.all(4)),
@ -133,7 +133,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const EnteSectionTitle(),
SectionTitle(titleWithBrand: getOnEnteSection(context)),
_sortMenu(),
],
),
@ -148,9 +148,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
TrashButtonWidget(trashAndHiddenTextStyle),
ArchivedCollectionsButtonWidget(trashAndHiddenTextStyle),
const SizedBox(height: 12),
HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
const SizedBox(height: 12),
TrashButtonWidget(trashAndHiddenTextStyle),
],
),
),

View file

@ -1,14 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class EnteLoadingWidget extends StatelessWidget {
const EnteLoadingWidget({Key? key}) : super(key: key);
final Color? color;
const EnteLoadingWidget({this.color, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox.fromSize(
size: const Size.square(30),
child: const CupertinoActivityIndicator(),
child: Padding(
padding: const EdgeInsets.all(5),
child: SizedBox.fromSize(
size: const Size.square(14),
child: CircularProgressIndicator(
strokeWidth: 2,
color: color ?? getEnteColorScheme(context).strokeBase,
),
),
),
);
}

View file

@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
enum SizeVarient { small, medium, large }
extension ExtraSizeVarient on SizeVarient {
double size() {
if (this == SizeVarient.small) {
return 21;
} else if (this == SizeVarient.medium) {
return 24;
} else if (this == SizeVarient.large) {
return 28;
}
return -1;
}
}
class BrandTitleWidget extends StatelessWidget {
final SizeVarient size;
const BrandTitleWidget({required this.size, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
"ente",
style: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
fontSize: size.size(),
),
);
}
}

View file

@ -23,7 +23,7 @@ class CaptionedTextWidget extends StatelessWidget {
return Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 2),
child: Row(
children: [
Flexible(

View file

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
enum DividerType {
solid,
menu,
menuNoIcon,
bottomBar,
}
class DividerWidget extends StatelessWidget {
final DividerType dividerType;
final Color bgColor;
const DividerWidget({
required this.dividerType,
this.bgColor = Colors.transparent,
super.key,
});
@override
Widget build(BuildContext context) {
final dividerColor = getEnteColorScheme(context).blurStrokeFaint;
if (dividerType == DividerType.solid) {
return Container(
color: getEnteColorScheme(context).strokeFaint,
width: double.infinity,
height: 1,
);
}
if (dividerType == DividerType.bottomBar) {
return Container(
color: dividerColor,
width: double.infinity,
height: 1,
);
}
return Row(
children: [
Container(
color: bgColor,
width: dividerType == DividerType.menu
? 48
: dividerType == DividerType.menuNoIcon
? 16
: 0,
height: 1,
),
Expanded(
child: Container(
color: dividerColor,
height: 1,
width: double.infinity,
),
),
],
);
}
}

View file

@ -44,32 +44,38 @@ class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
MediaQuery.of(context).platformBrightness == Brightness.light
? enteColorScheme.backgroundElevated2
: enteColorScheme.backgroundElevated;
return AnimatedContainer(
curve: Curves.ease,
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: expandableController.value ? backgroundColor : null,
borderRadius: BorderRadius.circular(4),
),
child: ExpandableNotifier(
controller: expandableController,
child: ScrollOnExpand(
child: ExpandablePanel(
header: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: widget.title,
makeTextBold: true,
return Padding(
padding: EdgeInsets.only(bottom: expandableController.value ? 8 : 0),
child: AnimatedContainer(
curve: Curves.ease,
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: expandableController.value ? backgroundColor : null,
borderRadius: BorderRadius.circular(4),
),
child: ExpandableNotifier(
controller: expandableController,
child: ScrollOnExpand(
child: ExpandablePanel(
header: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: widget.title,
makeTextBold: true,
),
isExpandable: true,
leadingIcon: widget.leadingIcon,
trailingIcon: Icons.expand_more,
menuItemColor: enteColorScheme.fillFaint,
expandableController: expandableController,
),
isHeaderOfExpansion: true,
leadingIcon: widget.leadingIcon,
trailingIcon: Icons.expand_more,
menuItemColor: enteColorScheme.fillFaint,
expandableController: expandableController,
collapsed: const SizedBox.shrink(),
expanded: Padding(
padding: const EdgeInsets.only(bottom: 4),
child: widget.selectionOptionsWidget,
),
theme: getExpandableTheme(context),
controller: expandableController,
),
collapsed: const SizedBox.shrink(),
expanded: widget.selectionOptionsWidget,
theme: getExpandableTheme(context),
controller: expandableController,
),
),
),

View file

@ -1,8 +1,7 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/opened_settings_event.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/viewer/search/search_widget.dart';
class HomeHeaderWidget extends StatefulWidget {
@ -17,30 +16,23 @@ class HomeHeaderWidget extends StatefulWidget {
class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
@override
Widget build(BuildContext context) {
final hasNotch = window.viewPadding.top > 65;
return Padding(
padding: EdgeInsets.fromLTRB(4, hasNotch ? 4 : 8, 4, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
onPressed: () {
Scaffold.of(context).openDrawer();
Bus.instance.fire(OpenedSettingsEvent());
},
splashColor: Colors.transparent,
icon: const Icon(
Icons.menu_outlined,
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: widget.centerWidget,
),
const SearchIconWidget(),
],
),
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButtonWidget(
iconButtonType: IconButtonType.primary,
icon: Icons.menu_outlined,
onTap: () {
Scaffold.of(context).openDrawer();
Bus.instance.fire(OpenedSettingsEvent());
},
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: widget.centerWidget,
),
const SearchIconWidget(),
],
);
}
}

View file

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
enum IconButtonType {
primary,
secondary,
rounded,
}
class IconButtonWidget extends StatefulWidget {
final IconButtonType iconButtonType;
final IconData icon;
final bool disableGestureDetector;
final VoidCallback? onTap;
final Color? defaultColor;
final Color? pressedColor;
final Color? iconColor;
const IconButtonWidget({
required this.icon,
required this.iconButtonType,
this.disableGestureDetector = false,
this.onTap,
this.defaultColor,
this.pressedColor,
this.iconColor,
super.key,
});
@override
State<IconButtonWidget> createState() => _IconButtonWidgetState();
}
class _IconButtonWidgetState extends State<IconButtonWidget> {
Color? iconStateColor;
@override
void didChangeDependencies() {
setState(() {
iconStateColor = null;
});
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
iconStateColor ??
(iconStateColor = widget.defaultColor ??
(widget.iconButtonType == IconButtonType.rounded
? colorTheme.fillFaint
: null));
return widget.disableGestureDetector
? _iconButton(colorTheme)
: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: widget.onTap,
child: _iconButton(colorTheme),
);
}
Widget _iconButton(EnteColorScheme colorTheme) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 20),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: iconStateColor,
),
child: Icon(
widget.icon,
color: widget.iconColor ??
(widget.iconButtonType == IconButtonType.secondary
? colorTheme.strokeMuted
: colorTheme.strokeBase),
size: 24,
),
),
);
}
_onTapDown(details) {
final colorTheme = getEnteColorScheme(context);
setState(() {
iconStateColor = widget.pressedColor ??
(widget.iconButtonType == IconButtonType.rounded
? colorTheme.fillMuted
: colorTheme.fillFaint);
});
}
_onTapUp(details) {
Future.delayed(const Duration(milliseconds: 100), () {
setState(() {
iconStateColor = null;
});
});
}
_onTapCancel() {
setState(() {
iconStateColor = null;
});
}
}

View file

@ -4,11 +4,15 @@ import 'package:photos/ente_theme_data.dart';
class MenuItemWidget extends StatefulWidget {
final Widget captionedTextWidget;
final bool isHeaderOfExpansion;
// leading icon can be passed without specifing size of icon, this component sets size to 20x20 irrespective of passed icon's size
final bool isExpandable;
/// leading icon can be passed without specifing size of icon,
/// this component sets size to 20x20 irrespective of passed icon's size
final IconData? leadingIcon;
final Color? leadingIconColor;
// trailing icon can be passed without size as default size set by flutter is what this component expects
/// trailing icon can be passed without size as default size set by
/// flutter is what this component expects
final IconData? trailingIcon;
final Widget? trailingSwitch;
final bool trailingIconIsMuted;
@ -17,10 +21,16 @@ class MenuItemWidget extends StatefulWidget {
final Color? menuItemColor;
final bool alignCaptionedTextToLeft;
final double borderRadius;
final Color? pressedColor;
final ExpandableController? expandableController;
final bool isBottomBorderRadiusRemoved;
final bool isTopBorderRadiusRemoved;
/// disable gesture detector if not used
final bool isGestureDetectorDisabled;
const MenuItemWidget({
required this.captionedTextWidget,
this.isHeaderOfExpansion = false,
this.isExpandable = false,
this.leadingIcon,
this.leadingIconColor,
this.trailingIcon,
@ -31,7 +41,11 @@ class MenuItemWidget extends StatefulWidget {
this.menuItemColor,
this.alignCaptionedTextToLeft = false,
this.borderRadius = 4.0,
this.pressedColor,
this.expandableController,
this.isBottomBorderRadiusRemoved = false,
this.isTopBorderRadiusRemoved = false,
this.isGestureDetectorDisabled = false,
Key? key,
}) : super(key: key);
@ -40,8 +54,10 @@ class MenuItemWidget extends StatefulWidget {
}
class _MenuItemWidgetState extends State<MenuItemWidget> {
Color? menuItemColor;
@override
void initState() {
menuItemColor = widget.menuItemColor;
if (widget.expandableController != null) {
widget.expandableController!.addListener(() {
setState(() {});
@ -50,6 +66,12 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
super.initState();
}
@override
void didChangeDependencies() {
menuItemColor = widget.menuItemColor;
super.didChangeDependencies();
}
@override
void dispose() {
if (widget.expandableController != null) {
@ -60,11 +82,14 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
@override
Widget build(BuildContext context) {
return widget.isHeaderOfExpansion
return widget.isExpandable || widget.isGestureDetectorDisabled
? menuItemWidget(context)
: GestureDetector(
onTap: widget.onTap,
onDoubleTap: widget.onDoubleTap,
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onCancel,
child: menuItemWidget(context),
);
}
@ -73,21 +98,25 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
final borderRadius = Radius.circular(widget.borderRadius);
final isExpanded = widget.expandableController?.value;
final bottomBorderRadius = isExpanded != null && isExpanded
final bottomBorderRadius =
(isExpanded != null && isExpanded) || widget.isBottomBorderRadiusRemoved
? const Radius.circular(0)
: borderRadius;
final topBorderRadius = widget.isTopBorderRadiusRemoved
? const Radius.circular(0)
: borderRadius;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
duration: const Duration(milliseconds: 20),
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.only(left: 16, right: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: borderRadius,
topRight: borderRadius,
topLeft: topBorderRadius,
topRight: topBorderRadius,
bottomLeft: bottomBorderRadius,
bottomRight: bottomBorderRadius,
),
color: widget.menuItemColor,
color: menuItemColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -139,4 +168,25 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
),
);
}
void _onTapDown(details) {
setState(() {
menuItemColor = widget.pressedColor ?? widget.menuItemColor;
});
}
void _onTapUp(details) {
Future.delayed(
const Duration(milliseconds: 100),
() => setState(() {
menuItemColor = widget.menuItemColor;
}),
);
}
void _onCancel() {
setState(() {
menuItemColor = widget.menuItemColor;
});
}
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class MenuSectionDescriptionWidget extends StatelessWidget {
final String content;
const MenuSectionDescriptionWidget({required this.content, super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
child: Text(
content,
style: getEnteTextTheme(context)
.mini
.copyWith(color: getEnteColorScheme(context).textMuted),
),
);
}
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/text_style.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
class NotificationWarningWidget extends StatelessWidget {
final IconData warningIcon;
@ -33,8 +34,9 @@ class NotificationWarningWidget extends StatelessWidget {
color: warning500,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
warningIcon,
@ -50,23 +52,14 @@ class NotificationWarningWidget extends StatelessWidget {
),
),
const SizedBox(width: 12),
ClipOval(
child: Material(
color: fillFaintDark,
child: InkWell(
splashColor: Colors.red, // Splash color
onTap: onTap,
child: SizedBox(
width: 40,
height: 40,
child: Icon(
actionIcon,
color: Colors.white,
),
),
),
),
),
IconButtonWidget(
icon: actionIcon,
iconButtonType: IconButtonType.rounded,
iconColor: strokeBaseDark,
defaultColor: fillFaintDark,
pressedColor: fillMutedDark,
onTap: onTap,
)
],
),
),

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class TitleBarTitleWidget extends StatelessWidget {
final String? title;
final bool isTitleH2;
final IconData? icon;
const TitleBarTitleWidget({
this.title,
this.isTitleH2 = false,
this.icon,
super.key,
});
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorTheme = getEnteColorScheme(context);
if (title != null) {
if (icon != null) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
title!,
style: textTheme.h3Bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(width: 8),
Icon(icon, size: 20, color: colorTheme.strokeMuted),
],
);
}
if (isTitleH2) {
return Text(
title!,
style: textTheme.h2Bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
} else {
return Text(
title!,
style: textTheme.h3Bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
}
}
return const SizedBox.shrink();
}
}

View file

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
class TitleBarWidget extends StatelessWidget {
final IconButtonWidget? leading;
final String? title;
final String? caption;
final Widget? flexibleSpaceTitle;
final String? flexibleSpaceCaption;
final List<Widget>? actionIcons;
final bool isTitleH2WithoutLeading;
final bool isFlexibleSpaceDisabled;
final bool isOnTopOfScreen;
const TitleBarWidget({
this.leading,
this.title,
this.caption,
this.flexibleSpaceTitle,
this.flexibleSpaceCaption,
this.actionIcons,
this.isTitleH2WithoutLeading = false,
this.isFlexibleSpaceDisabled = false,
this.isOnTopOfScreen = true,
super.key,
});
@override
Widget build(BuildContext context) {
const toolbarHeight = 48.0;
final textTheme = getEnteTextTheme(context);
final colorTheme = getEnteColorScheme(context);
return SliverAppBar(
primary: isOnTopOfScreen ? true : false,
toolbarHeight: toolbarHeight,
leadingWidth: 48,
automaticallyImplyLeading: false,
pinned: true,
expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102,
centerTitle: false,
titleSpacing: 4,
title: Padding(
padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
title == null
? const SizedBox.shrink()
: Text(
title!,
style: isTitleH2WithoutLeading
? textTheme.h2Bold
: textTheme.largeBold,
),
caption == null || isTitleH2WithoutLeading
? const SizedBox.shrink()
: Text(
caption!,
style: textTheme.mini.copyWith(color: colorTheme.textMuted),
)
],
),
),
actions: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
children: _actionsWithPaddingInBetween(),
),
),
],
leading: isTitleH2WithoutLeading
? null
: leading ??
IconButtonWidget(
icon: Icons.arrow_back_outlined,
iconButtonType: IconButtonType.primary,
onTap: () {
Navigator.pop(context);
},
),
flexibleSpace: isFlexibleSpaceDisabled
? null
: FlexibleSpaceBar(
background: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(height: toolbarHeight),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
flexibleSpaceTitle == null
? const SizedBox.shrink()
: flexibleSpaceTitle!,
flexibleSpaceCaption == null
? const SizedBox.shrink()
: Text(
flexibleSpaceCaption!,
style: textTheme.small.copyWith(
color: colorTheme.textMuted,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
)
],
),
),
],
),
),
),
);
}
_actionsWithPaddingInBetween() {
if (actionIcons == null) {
return <Widget>[const SizedBox.shrink()];
}
final actions = <Widget>[];
bool addWhiteSpace = false;
final length = actionIcons!.length;
int index = 0;
if (length == 0) {
return <Widget>[const SizedBox.shrink()];
}
if (length == 1) {
return actionIcons;
}
while (index < length) {
if (!addWhiteSpace) {
actions.add(actionIcons![index]);
index++;
addWhiteSpace = true;
} else {
actions.add(const SizedBox(width: 4));
addWhiteSpace = false;
}
}
return actions;
}
}

View file

@ -1,10 +1,19 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/utils/debouncer.dart';
typedef OnChangedCallBack = void Function(bool);
enum ExecutionState {
idle,
inProgress,
successful,
}
typedef OnChangedCallBack = Future<void> Function();
typedef ValueCallBack = bool Function();
class ToggleSwitchWidget extends StatefulWidget {
final bool value;
final ValueCallBack value;
final OnChangedCallBack onChanged;
const ToggleSwitchWidget({
required this.value,
@ -17,24 +26,112 @@ class ToggleSwitchWidget extends StatefulWidget {
}
class _ToggleSwitchWidgetState extends State<ToggleSwitchWidget> {
late bool toggleValue;
ExecutionState executionState = ExecutionState.idle;
final _debouncer = Debouncer(const Duration(milliseconds: 300));
@override
void initState() {
toggleValue = widget.value.call();
super.initState();
}
@override
Widget build(BuildContext context) {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: SizedBox(
height: 30,
child: FittedBox(
fit: BoxFit.contain,
child: Switch.adaptive(
activeColor: enteColorScheme.primary400,
inactiveTrackColor: enteColorScheme.fillMuted,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: widget.value,
onChanged: widget.onChanged,
final Widget stateIcon = _stateIcon(enteColorScheme);
return Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 2),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 175),
switchInCurve: Curves.easeInExpo,
switchOutCurve: Curves.easeOutExpo,
child: stateIcon,
),
),
),
SizedBox(
height: 31,
child: FittedBox(
fit: BoxFit.contain,
child: Switch.adaptive(
activeColor: enteColorScheme.primary400,
inactiveTrackColor: enteColorScheme.fillMuted,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: toggleValue,
onChanged: (negationOfToggleValue) async {
setState(() {
toggleValue = negationOfToggleValue;
//start showing inProgress statu icons if toggle takes more than debounce time
_debouncer.run(
() => Future(
() {
setState(() {
executionState = ExecutionState.inProgress;
});
},
),
);
});
final Stopwatch stopwatch = Stopwatch()..start();
await widget.onChanged.call();
//for toggle feedback on short unsuccessful onChanged
await _feedbackOnUnsuccessfulToggle(stopwatch);
//debouncer gets canceled if onChanged takes less than debounce time
_debouncer.cancelDebounce();
setState(() {
final newValue = widget.value.call();
//if onchanged on toggle is successful
if (toggleValue == newValue) {
if (executionState == ExecutionState.inProgress) {
executionState = ExecutionState.successful;
Future.delayed(const Duration(seconds: 2), () {
setState(() {
executionState = ExecutionState.idle;
});
});
}
} else {
toggleValue = !toggleValue;
executionState = ExecutionState.idle;
}
});
},
),
),
),
],
);
}
Widget _stateIcon(enteColorScheme) {
if (executionState == ExecutionState.idle) {
return const SizedBox(width: 24);
} else if (executionState == ExecutionState.inProgress) {
return EnteLoadingWidget(
color: enteColorScheme.strokeMuted,
);
} else if (executionState == ExecutionState.successful) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Icon(
Icons.check_outlined,
size: 22,
color: enteColorScheme.primary500,
),
);
} else {
return const SizedBox(width: 24);
}
}
Future<void> _feedbackOnUnsuccessfulToggle(Stopwatch stopwatch) async {
final timeElapsed = stopwatch.elapsedMilliseconds;
if (timeElapsed < 200) {
await Future.delayed(
Duration(milliseconds: 200 - timeElapsed),
);
}
}
}

View file

@ -3,6 +3,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/collection.dart';
@ -23,7 +24,7 @@ import 'package:photos/utils/share_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
enum CollectionActionType { addFiles, moveFiles, restoreFiles }
enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
String _actionName(CollectionActionType type, bool plural) {
final titleSuffix = (plural ? "s" : "");
@ -38,6 +39,9 @@ String _actionName(CollectionActionType type, bool plural) {
case CollectionActionType.restoreFiles:
text = "Restore file";
break;
case CollectionActionType.unHide:
text = "Unhide file";
break;
}
return text + titleSuffix;
}
@ -189,8 +193,16 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
}
Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails();
final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
final latestCollectionFiles =
await CollectionsService.instance.getLatestCollectionFiles();
for (final file in latestCollectionFiles) {
final c =
CollectionsService.instance.getCollectionByID(file.collectionID);
if (c.owner.id == Configuration.instance.getUserID() && !c.isHidden()) {
collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
}
}
collectionsWithThumbnail.sort((first, second) {
return compareAsciiLowerCaseNatural(
first.collection.name ?? "",
@ -273,6 +285,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
return _addToCollection(collectionID);
case CollectionActionType.moveFiles:
return _moveFilesToCollection(collectionID);
case CollectionActionType.unHide:
return _moveFilesToCollection(collectionID);
case CollectionActionType.restoreFiles:
return _restoreFilesToCollection(collectionID);
}
@ -280,7 +294,10 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
}
Future<bool> _moveFilesToCollection(int toCollectionID) async {
final dialog = createProgressDialog(context, "Moving files to album...");
final String message = widget.actionType == CollectionActionType.moveFiles
? "Moving files to album..."
: "Unhiding files to album";
final dialog = createProgressDialog(context, message);
await dialog.show();
try {
final int fromCollectionID =

View file

@ -0,0 +1,26 @@
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:photos/ui/home/memories_widget.dart';
import 'package:photos/ui/home/status_bar_widget.dart';
class HeaderWidget extends StatelessWidget {
static const _memoriesWidget = MemoriesWidget();
static const _statusBarWidget = StatusBarWidget();
const HeaderWidget({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Logger("Header").info("Building header widget");
const list = [
_statusBarWidget,
_memoriesWidget,
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: list,
);
}
}

View file

@ -0,0 +1,189 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/effects.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/nav_bar.dart';
class HomeBottomNavigationBar extends StatefulWidget {
const HomeBottomNavigationBar(
this.selectedFiles, {
required this.selectedTabIndex,
Key? key,
}) : super(key: key);
final SelectedFiles selectedFiles;
final int selectedTabIndex;
@override
State<HomeBottomNavigationBar> createState() =>
_HomeBottomNavigationBarState();
}
class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
int currentTabIndex = 0;
@override
void initState() {
super.initState();
currentTabIndex = widget.selectedTabIndex;
widget.selectedFiles.addListener(() {
setState(() {});
});
_tabChangedEventSubscription =
Bus.instance.on<TabChangedEvent>().listen((event) {
if (event.source != TabChangedEventSource.tabBar) {
debugPrint(
'${(TabChangedEvent).toString()} index changed from '
'$currentTabIndex to ${event.selectedIndex} via ${event.source}',
);
if (mounted) {
setState(() {
currentTabIndex = event.selectedIndex;
});
}
} else if (event.source == TabChangedEventSource.tabBar &&
currentTabIndex == event.selectedIndex) {
// user tapped on the currently selected index on the tapBar
Bus.instance.fire(TabDoubleTapEvent(currentTabIndex));
}
});
}
@override
void dispose() {
_tabChangedEventSubscription.cancel();
super.dispose();
}
void _onTabChange(int index, {String mode = 'tabChanged'}) {
debugPrint("_TabChanged called via method $mode");
Bus.instance.fire(
TabChangedEvent(
index,
TabChangedEventSource.tabBar,
),
);
}
@override
Widget build(BuildContext context) {
final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty;
final enteColorScheme = getEnteColorScheme(context);
final navBarBlur =
MediaQuery.of(context).platformBrightness == Brightness.light
? blurBase
: blurMuted;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: filesAreSelected ? 0 : 56,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: filesAreSelected ? 0.0 : 1.0,
curve: Curves.easeIn,
child: IgnorePointer(
ignoring: filesAreSelected,
child: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(32),
child: Container(
alignment: Alignment.bottomCenter,
height: 48,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: navBarBlur,
sigmaY: navBarBlur,
),
child: GNav(
curve: Curves.easeOutExpo,
backgroundColor:
getEnteColorScheme(context).fillMuted,
mainAxisAlignment: MainAxisAlignment.center,
rippleColor: Colors.white.withOpacity(0.1),
activeColor: Theme.of(context)
.colorScheme
.gNavBarActiveColor,
iconSize: 24,
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
duration: const Duration(milliseconds: 200),
gap: 0,
tabBorderRadius: 32,
tabBackgroundColor: Theme.of(context)
.colorScheme
.gNavBarActiveColor,
haptic: false,
tabs: [
GButton(
margin: const EdgeInsets.fromLTRB(8, 6, 10, 6),
icon: Icons.home_rounded,
iconColor: enteColorScheme.tabIcon,
iconActiveColor: strokeBaseLight,
text: '',
onPressed: () {
_onTabChange(
0,
mode: "OnPressed",
); // To take care of occasional missing events
},
),
GButton(
margin: const EdgeInsets.fromLTRB(10, 6, 10, 6),
icon: Icons.collections_rounded,
iconColor: enteColorScheme.tabIcon,
iconActiveColor: strokeBaseLight,
text: '',
onPressed: () {
_onTabChange(
1,
mode: "OnPressed",
); // To take care of occasional missing
// events
},
),
GButton(
margin: const EdgeInsets.fromLTRB(10, 6, 8, 6),
icon: Icons.people_outlined,
iconColor: enteColorScheme.tabIcon,
iconActiveColor: strokeBaseLight,
text: '',
onPressed: () {
_onTabChange(
2,
mode: "OnPressed",
); // To take care
// of occasional missing events
},
),
],
selectedIndex: currentTabIndex,
onTabChange: _onTabChange,
),
),
),
),
),
],
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,88 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/backup_folders_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart';
class HomeGalleryWidget extends StatelessWidget {
final Widget header;
final Widget footer;
final SelectedFiles selectedFiles;
const HomeGalleryWidget({
Key key,
this.header,
this.footer,
this.selectedFiles,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final double bottomSafeArea = MediaQuery.of(context).padding.bottom;
final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
final ownerID = Configuration.instance.getUserID();
final hasSelectedAllForBackup =
Configuration.instance.hasSelectedAllFoldersForBackup();
final collectionsToHide =
CollectionsService.instance.collectionsHiddenFromTimeline();
FileLoadResult result;
if (hasSelectedAllForBackup) {
result = await FilesDB.instance.getAllLocalAndUploadedFiles(
creationStartTime,
creationEndTime,
ownerID,
limit: limit,
asc: asc,
ignoredCollectionIDs: collectionsToHide,
);
} else {
result = await FilesDB.instance.getAllPendingOrUploadedFiles(
creationStartTime,
creationEndTime,
ownerID,
limit: limit,
asc: asc,
ignoredCollectionIDs: collectionsToHide,
);
}
// hide ignored files from home page UI
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
result.files.removeWhere(
(f) =>
f.uploadedFileID == null &&
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
);
return result;
},
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
EventType.archived,
EventType.hide,
},
forceReloadEvents: [
Bus.instance.on<BackupFoldersUpdatedEvent>(),
Bus.instance.on<ForceReloadHomeGalleryEvent>(),
],
tagPrefix: "home_gallery",
selectedFiles: selectedFiles,
header: header,
footer: footer,
// scrollSafe area -> SafeArea + Preserver more + Nav Bar buttons
scrollBottomSafeArea: bottomSafeArea + 180,
);
return gallery;
}
}

View file

@ -410,9 +410,11 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
extents: 1,
onPageChanged: (index) async {
await MemoriesService.instance.markMemoryAsSeen(widget.memories[index]);
setState(() {
_index = index;
});
if (mounted) {
setState(() {
_index = index;
});
}
},
physics: _shouldDisableScroll
? const NeverScrollableScrollPhysics()

View file

@ -1,5 +1,3 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/services/local_sync_service.dart';
@ -7,8 +5,8 @@ import 'package:photos/ui/backup_folder_selection_page.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/utils/navigation_util.dart';
class GalleryFooterWidget extends StatelessWidget {
const GalleryFooterWidget({Key key}) : super(key: key);
class PreserveFooterWidget extends StatelessWidget {
const PreserveFooterWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/ui/backup_folder_selection_page.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/utils/navigation_util.dart';
class StartBackupHookWidget extends StatelessWidget {
final Widget headerWidget;
const StartBackupHookWidget({super.key, required this.headerWidget});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
headerWidget,
Padding(
padding: const EdgeInsets.only(top: 64),
child: Image.asset(
"assets/onboarding_safe.png",
height: 206,
),
),
Text(
'No photos are being backed up right now',
style: Theme.of(context)
.textTheme
.caption!
.copyWith(fontFamily: 'Inter-Medium', fontSize: 16),
),
Center(
child: Material(
type: MaterialType.transparency,
child: Container(
width: double.infinity,
height: 64,
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: GradientButton(
onTap: () async {
if (LocalSyncService.instance
.hasGrantedLimitedPermissions()) {
PhotoManager.presentLimited();
} else {
routeToPage(
context,
const BackupFolderSelectionPage(
buttonText: "Start backup",
),
);
}
},
text: "Start backup",
),
),
),
),
const Padding(padding: EdgeInsets.all(50)),
],
);
}
}

View file

@ -9,11 +9,11 @@ import 'package:photos/events/notification_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/services/user_remote_flag_service.dart';
import 'package:photos/theme/text_style.dart';
import 'package:photos/ui/account/verify_recovery_page.dart';
import 'package:photos/ui/components/brand_title_widget.dart';
import 'package:photos/ui/components/home_header_widget.dart';
import 'package:photos/ui/components/notification_warning_widget.dart';
import 'package:photos/ui/header_error_widget.dart';
import 'package:photos/ui/home/header_error_widget.dart';
import 'package:photos/utils/navigation_util.dart';
const double kContainerHeight = 36;
@ -84,9 +84,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
HomeHeaderWidget(
centerWidget: _showStatus
? _showErrorBanner
? const BrandTitleWidget(size: SizeVarient.medium)
? const Text("ente", style: brandStyleMedium)
: const SyncStatusWidget()
: const BrandTitleWidget(size: SizeVarient.medium),
: const Text("ente", style: brandStyleMedium),
),
AnimatedOpacity(
opacity: _showErrorBanner ? 1 : 0,
@ -100,9 +100,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
: const SizedBox.shrink(),
UserRemoteFlagService.instance.shouldShowRecoveryVerification()
? NotificationWarningWidget(
warningIcon: Icons.gpp_maybe,
warningIcon: Icons.error_outline,
actionIcon: Icons.arrow_forward,
text: "Please ensure you have your 24 word recovery key",
text: "Confirm your recovery key",
onTap: () async => {
await routeToPage(
context,

View file

@ -2,62 +2,48 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:move_to_background/move_to_background.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/events/account_configured_event.dart';
import 'package:photos/events/backup_folders_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/permission_granted_event.dart';
import 'package:photos/events/subscription_purchased_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/events/trigger_logout_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/effects.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/backup_folder_selection_page.dart';
import 'package:photos/ui/collections_gallery_widget.dart';
import 'package:photos/ui/common/bottom_shadow.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/extents_page_view.dart';
import 'package:photos/ui/grant_permissions_widget.dart';
import 'package:photos/ui/landing_page_widget.dart';
import 'package:photos/ui/home/grant_permissions_widget.dart';
import 'package:photos/ui/home/header_widget.dart';
import 'package:photos/ui/home/home_bottom_nav_bar.dart';
import 'package:photos/ui/home/home_gallery_widget.dart';
import 'package:photos/ui/home/landing_page_widget.dart';
import 'package:photos/ui/home/preserve_footer_widget.dart';
import 'package:photos/ui/home/start_backup_hook_widget.dart';
import 'package:photos/ui/loading_photos_widget.dart';
import 'package:photos/ui/memories_widget.dart';
import 'package:photos/ui/nav_bar.dart';
import 'package:photos/ui/settings/app_update_dialog.dart';
import 'package:photos/ui/settings_page.dart';
import 'package:photos/ui/shared_collections_gallery.dart';
import 'package:photos/ui/status_bar_widget.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart';
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
import 'package:photos/ui/viewer/gallery/gallery_footer_widget.dart';
import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:uni_links/uni_links.dart';
@ -81,7 +67,6 @@ class _HomeWidgetState extends State<HomeWidget> {
final PageController _pageController = PageController();
int _selectedTabIndex = 0;
Widget _headerWidgetWithSettingsButton;
// for receiving media files
// ignore: unused_field
@ -100,15 +85,14 @@ class _HomeWidgetState extends State<HomeWidget> {
@override
void initState() {
_logger.info("Building initstate");
_headerWidgetWithSettingsButton = Stack(
children: const [
_headerWidget,
],
);
_tabChangedEventSubscription =
Bus.instance.on<TabChangedEvent>().listen((event) {
if (event.source != TabChangedEventSource.pageView) {
debugPrint(
"TabChange going from $_selectedTabIndex to ${event.selectedIndex} souce: ${event.source}",
);
_selectedTabIndex = event.selectedIndex;
// _pageController.jumpToPage(_selectedTabIndex);
_pageController.animateToPage(
event.selectedIndex,
duration: const Duration(milliseconds: 100),
@ -126,34 +110,7 @@ class _HomeWidgetState extends State<HomeWidget> {
});
_triggerLogoutEvent =
Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
final AlertDialog alert = AlertDialog(
title: const Text("Session expired"),
content: const Text("Please login again"),
actions: [
TextButton(
child: Text(
"Ok",
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
final dialog = createProgressDialog(context, "Logging out...");
await dialog.show();
await Configuration.instance.logout();
await dialog.hide();
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
await _autoLogoutAlert();
});
_loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
_logger.info('logged out, selectTab index to 0');
@ -218,6 +175,37 @@ class _HomeWidgetState extends State<HomeWidget> {
super.initState();
}
Future<void> _autoLogoutAlert() async {
final AlertDialog alert = AlertDialog(
title: const Text("Session expired"),
content: const Text("Please login again"),
actions: [
TextButton(
child: Text(
"Ok",
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
final dialog = createProgressDialog(context, "Logging out...");
await dialog.show();
await Configuration.instance.logout();
await dialog.hide();
},
),
],
);
await showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
@override
void dispose() {
_tabChangedEventSubscription.cancel();
@ -262,8 +250,8 @@ class _HomeWidgetState extends State<HomeWidget> {
child: WillPopScope(
child: Scaffold(
drawerScrimColor: getEnteColorScheme(context).strokeFainter,
drawerEnableOpenDragGesture:
false, //using a hack instead of enabling this as enabling this will create other problems
drawerEnableOpenDragGesture: false,
//using a hack instead of enabling this as enabling this will create other problems
drawer: enableDrawer
? ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 428),
@ -345,8 +333,12 @@ class _HomeWidgetState extends State<HomeWidget> {
physics: const BouncingScrollPhysics(),
children: [
showBackupFolderHook
? _getBackupFolderSelectionHook()
: _getMainGalleryWidget(),
? const StartBackupHookWidget(headerWidget: _headerWidget)
: HomeGalleryWidget(
header: _headerWidget,
footer: const PreserveFooterWidget(),
selectedFiles: _selectedFiles,
),
_deviceFolderGalleryWidget,
_sharedCollectionGallery,
],
@ -422,347 +414,4 @@ class _HomeWidgetState extends State<HomeWidget> {
final ott = Uri.parse(link).queryParameters["ott"];
UserService.instance.verifyEmail(context, ott);
}
Widget _getMainGalleryWidget() {
Widget header;
if (_selectedFiles.files.isEmpty) {
header = _headerWidgetWithSettingsButton;
} else {
header = _headerWidget;
}
final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
final ownerID = Configuration.instance.getUserID();
final hasSelectedAllForBackup =
Configuration.instance.hasSelectedAllFoldersForBackup();
final archivedCollectionIds =
CollectionsService.instance.getArchivedCollections();
FileLoadResult result;
if (hasSelectedAllForBackup) {
result = await FilesDB.instance.getAllLocalAndUploadedFiles(
creationStartTime,
creationEndTime,
ownerID,
limit: limit,
asc: asc,
ignoredCollectionIDs: archivedCollectionIds,
);
} else {
result = await FilesDB.instance.getAllPendingOrUploadedFiles(
creationStartTime,
creationEndTime,
ownerID,
limit: limit,
asc: asc,
ignoredCollectionIDs: archivedCollectionIds,
);
}
// hide ignored files from home page UI
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
result.files.removeWhere(
(f) =>
f.uploadedFileID == null &&
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
);
return result;
},
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
EventType.archived,
},
forceReloadEvents: [
Bus.instance.on<BackupFoldersUpdatedEvent>(),
Bus.instance.on<ForceReloadHomeGalleryEvent>(),
],
tagPrefix: "home_gallery",
selectedFiles: _selectedFiles,
header: header,
footer: const GalleryFooterWidget(),
);
return Stack(
children: [
Container(
child: gallery,
),
HomePageAppBar(_selectedFiles),
],
);
}
Widget _getBackupFolderSelectionHook() {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_headerWidgetWithSettingsButton,
Padding(
padding: const EdgeInsets.only(top: 64),
child: Image.asset(
"assets/onboarding_safe.png",
height: 206,
),
),
Text(
'No photos are being backed up right now',
style: Theme.of(context)
.textTheme
.caption
.copyWith(fontFamily: 'Inter-Medium', fontSize: 16),
),
Center(
child: Material(
type: MaterialType.transparency,
child: Container(
width: double.infinity,
height: 64,
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: GradientButton(
onTap: () async {
if (LocalSyncService.instance
.hasGrantedLimitedPermissions()) {
PhotoManager.presentLimited();
} else {
routeToPage(
context,
const BackupFolderSelectionPage(
buttonText: "Start backup",
),
);
}
},
text: "Start backup",
),
),
),
),
const Padding(padding: EdgeInsets.all(50)),
],
);
}
}
class HomePageAppBar extends StatefulWidget {
const HomePageAppBar(
this.selectedFiles, {
Key key,
}) : super(key: key);
final SelectedFiles selectedFiles;
@override
State<HomePageAppBar> createState() => _HomePageAppBarState();
}
class _HomePageAppBarState extends State<HomePageAppBar> {
@override
void initState() {
super.initState();
widget.selectedFiles.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
final appBar = SizedBox(
height: 60,
child: GalleryAppBarWidget(
GalleryType.homepage,
null,
widget.selectedFiles,
),
);
if (widget.selectedFiles.files.isEmpty) {
return IgnorePointer(child: appBar);
} else {
return appBar;
}
}
}
class HomeBottomNavigationBar extends StatefulWidget {
const HomeBottomNavigationBar(
this.selectedFiles, {
this.selectedTabIndex,
Key key,
}) : super(key: key);
final SelectedFiles selectedFiles;
final int selectedTabIndex;
@override
State<HomeBottomNavigationBar> createState() =>
_HomeBottomNavigationBarState();
}
class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
int currentTabIndex = 0;
@override
void initState() {
super.initState();
currentTabIndex = widget.selectedTabIndex;
widget.selectedFiles.addListener(() {
setState(() {});
});
_tabChangedEventSubscription =
Bus.instance.on<TabChangedEvent>().listen((event) {
if (event.source != TabChangedEventSource.tabBar) {
debugPrint('index changed to ${event.selectedIndex}');
if (mounted) {
setState(() {
currentTabIndex = event.selectedIndex;
});
}
}
});
}
@override
void dispose() {
_tabChangedEventSubscription.cancel();
super.dispose();
}
void _onTabChange(int index) {
Bus.instance.fire(
TabChangedEvent(
index,
TabChangedEventSource.tabBar,
),
);
}
@override
Widget build(BuildContext context) {
final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty;
final enteColorScheme = getEnteColorScheme(context);
final navBarBlur =
MediaQuery.of(context).platformBrightness == Brightness.light
? blurBase
: blurMuted;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: filesAreSelected ? 0 : 56,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: filesAreSelected ? 0.0 : 1.0,
curve: Curves.easeIn,
child: IgnorePointer(
ignoring: filesAreSelected,
child: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(32),
child: Container(
alignment: Alignment.bottomCenter,
height: 48,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: navBarBlur,
sigmaY: navBarBlur,
),
child: GNav(
curve: Curves.easeOutExpo,
backgroundColor:
getEnteColorScheme(context).fillMuted,
mainAxisAlignment: MainAxisAlignment.center,
rippleColor: Colors.white.withOpacity(0.1),
activeColor: Theme.of(context)
.colorScheme
.gNavBarActiveColor,
iconSize: 24,
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
duration: const Duration(milliseconds: 200),
gap: 0,
tabBorderRadius: 32,
tabBackgroundColor: Theme.of(context)
.colorScheme
.gNavBarActiveColor,
haptic: false,
tabs: [
GButton(
margin: const EdgeInsets.fromLTRB(8, 6, 10, 6),
icon: Icons.home_rounded,
iconColor: enteColorScheme.tabIcon,
iconActiveColor: strokeBaseLight,
text: '',
onPressed: () {
_onTabChange(
0,
); // To take care of occasional missing events
},
),
GButton(
margin: const EdgeInsets.fromLTRB(10, 6, 10, 6),
icon: Icons.collections_rounded,
iconColor: enteColorScheme.tabIcon,
iconActiveColor: strokeBaseLight,
text: '',
onPressed: () {
_onTabChange(
1,
); // To take care of occasional missing events
},
),
GButton(
margin: const EdgeInsets.fromLTRB(10, 6, 8, 6),
icon: Icons.people_outlined,
iconColor: enteColorScheme.tabIcon,
iconActiveColor: strokeBaseLight,
text: '',
onPressed: () {
_onTabChange(
2,
); // To take care of occasional missing events
},
),
],
selectedIndex: currentTabIndex,
onTabChange: _onTabChange,
),
),
),
),
),
],
),
],
),
),
),
);
}
}
class HeaderWidget extends StatelessWidget {
static const _memoriesWidget = MemoriesWidget();
static const _statusBarWidget = StatusBarWidget();
const HeaderWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Logger("Header").info("Building header widget");
const list = [
_statusBarWidget,
_memoriesWidget,
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: list,
);
}
}

View file

@ -15,6 +15,7 @@ class DraggableScrollbar extends StatefulWidget {
final EdgeInsetsGeometry padding;
final int totalCount;
final int initialScrollIndex;
final double bottomSafeArea;
final int currentFirstIndex;
final ValueChanged<double> onChange;
final String Function(int) labelTextBuilder;
@ -26,6 +27,7 @@ class DraggableScrollbar extends StatefulWidget {
this.backgroundColor = Colors.white,
this.drawColor = Colors.grey,
this.heightScrollThumb = 80.0,
this.bottomSafeArea = 120,
this.padding,
this.totalCount = 1,
this.initialScrollIndex = 0,
@ -49,7 +51,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
double get thumbMin => 0.0;
double get thumbMax => context.size.height - widget.heightScrollThumb;
double get thumbMax =>
context.size.height - widget.heightScrollThumb - widget.bottomSafeArea;
AnimationController _thumbAnimationController;
Animation<double> _thumbAnimation;

View file

@ -38,6 +38,10 @@ class HugeListView<T> extends StatefulWidget {
/// Height of scroll thumb, defaults to 48.
final double thumbHeight;
/// Height of bottomSafeArea so that scroll thumb does not become hidden
/// or un-clickable due to footer elements. Default value is 120
final double bottomSafeArea;
/// Called to build an individual item with the specified [index].
final HugeListViewItemBuilder<T> itemBuilder;
@ -72,6 +76,7 @@ class HugeListView<T> extends StatefulWidget {
this.thumbBackgroundColor = Colors.red, // Colors.white,
this.thumbDrawColor = Colors.yellow, //Colors.grey,
this.thumbHeight = 48.0,
this.bottomSafeArea = 120.0,
this.isDraggableScrollbarEnabled = true,
this.thumbPadding,
}) : super(key: key);
@ -83,6 +88,7 @@ class HugeListView<T> extends StatefulWidget {
class HugeListViewState<T> extends State<HugeListView<T>> {
final scrollKey = GlobalKey<DraggableScrollbarState>();
final listener = ItemPositionsListener.create();
int lastIndexJump = -1;
dynamic error;
@override
@ -131,13 +137,27 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
totalCount: widget.totalCount,
initialScrollIndex: widget.startIndex,
onChange: (position) {
widget.controller
?.jumpTo(index: (position * widget.totalCount).floor());
final int currentIndex = _currentFirst();
final int floorIndex = (position * widget.totalCount).floor();
final int cielIndex = (position * widget.totalCount).ceil();
int nextIndexToJump;
if (floorIndex != currentIndex && floorIndex > currentIndex) {
nextIndexToJump = floorIndex;
} else if (cielIndex != currentIndex && cielIndex < currentIndex) {
nextIndexToJump = floorIndex;
} else {
return;
}
if (lastIndexJump != nextIndexToJump) {
lastIndexJump = nextIndexToJump;
widget.controller?.jumpTo(index: nextIndexToJump);
}
},
labelTextBuilder: widget.labelTextBuilder,
backgroundColor: widget.thumbBackgroundColor,
drawColor: widget.thumbDrawColor,
heightScrollThumb: widget.thumbHeight,
bottomSafeArea: widget.bottomSafeArea,
currentFirstIndex: _currentFirst(),
isEnabled: widget.isDraggableScrollbarEnabled,
padding: widget.thumbPadding,

View file

@ -2,8 +2,6 @@
library google_nav_bar;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -120,19 +118,7 @@ class _GNavState extends State<GNav> {
Colors.transparent,
duration: widget.duration ?? const Duration(milliseconds: 500),
onPressed: () {
if (!clickable) return;
setState(() {
selectedIndex = widget.tabs.indexOf(t);
clickable = false;
});
widget.onTabChange(selectedIndex);
Future.delayed(
widget.duration ?? const Duration(milliseconds: 500), () {
setState(() {
clickable = true;
});
});
widget.onTabChange(widget.tabs.indexOf(t));
},
),
)

View file

@ -44,7 +44,7 @@ class SkipSubscriptionWidget extends StatelessWidget {
BillingService.instance
.verifySubscription(freeProductID, "", paymentProvider: "ente");
},
child: const Text("Continue on free plan"),
child: const Text("Continue on free trial"),
),
);
}

View file

@ -3,7 +3,6 @@
import 'dart:async';
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';
@ -38,7 +37,6 @@ class StripeSubscriptionPage extends StatefulWidget {
}
class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
final _logger = Logger("StripeSubscriptionPage");
final _billingService = BillingService.instance;
final _userService = UserService.instance;
Subscription _currentSubscription;

View file

@ -94,7 +94,7 @@ class ValidityWidget extends StatelessWidget {
);
var message = "Renews on $endDate";
if (currentSubscription.productID == freeProductID) {
message = "Free plan valid till $endDate";
message = "Free trial valid till $endDate";
} else if (currentSubscription.attributes?.isCancelled ?? false) {
message = "Your subscription will be cancelled on $endDate";
}

View file

@ -368,7 +368,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
planWidgets.add(
SubscriptionPlanWidget(
storage: _freePlan.storage,
price: "free",
price: "Free trial",
period: "",
isActive: true,
),

View file

@ -19,7 +19,7 @@ class SubscriptionPlanWidget extends StatelessWidget {
String _displayPrice() {
final result = price + (period.isNotEmpty ? " / " + period : "");
return result.isNotEmpty ? result : "Trial plan";
return price.isNotEmpty ? result : "Free trial";
}
@override

View file

@ -2,6 +2,7 @@
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/web_page.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@ -47,6 +48,7 @@ class AboutSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Source code",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -61,6 +63,7 @@ class AboutSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Check for updates",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -111,6 +114,7 @@ class AboutMenuItemWidget extends StatelessWidget {
captionedTextWidget: CaptionedTextWidget(
title: title,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.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/change_email_dialog.dart';
import 'package:photos/ui/account/password_entry_page.dart';
import 'package:photos/ui/account/recovery_key_page.dart';
@ -34,6 +35,7 @@ class AccountSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Recovery key",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -67,6 +69,7 @@ class AccountSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Change email",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -92,6 +95,7 @@ class AccountSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Change password",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {

View file

@ -15,7 +15,6 @@ class AppVersionWidget extends StatefulWidget {
class _AppVersionWidgetState extends State<AppVersionWidget> {
static const kTapThresholdForInspector = 5;
static const kConsecutiveTapTimeWindowInMilliseconds = 2000;
static const kDummyDelayDurationInMilliseconds = 1500;
int _lastTap;
int _consecutiveTaps = 0;

View file

@ -3,18 +3,17 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/backup_status.dart';
import 'package:photos/models/duplicate_files.dart';
import 'package:photos/services/deduplication_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/backup_folder_selection_page.dart';
import 'package:photos/ui/common/dialogs.dart';
import 'package:photos/ui/backup_settings_screen.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.dart';
import 'package:photos/ui/components/toggle_switch_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/ui/tools/deduplicate_page.dart';
import 'package:photos/ui/tools/free_space_page.dart';
@ -48,6 +47,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
captionedTextWidget: const CaptionedTextWidget(
title: "Backed up folders",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
@ -62,66 +62,28 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backup over mobile data",
),
trailingSwitch: ToggleSwitchWidget(
value: Configuration.instance.shouldBackupOverMobileData(),
onChanged: (value) async {
Configuration.instance.setBackupOverMobileData(value);
setState(() {});
},
),
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backup videos",
),
trailingSwitch: ToggleSwitchWidget(
value: Configuration.instance.shouldBackupVideos(),
onChanged: (value) async {
Configuration.instance.setShouldBackupVideos(value);
setState(() {});
},
title: "Backup settings",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
routeToPage(
context,
const BackupSettingsScreen(),
);
},
),
sectionOptionSpacing,
];
if (Platform.isIOS) {
sectionOptions.addAll([
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Disable auto lock",
),
trailingSwitch: ToggleSwitchWidget(
value: Configuration.instance.shouldKeepDeviceAwake(),
onChanged: (value) async {
if (value) {
final choice = await showChoiceDialog(
context,
"Disable automatic screen lock when ente is running?",
"This will ensure faster uploads by ensuring your device does not sleep when uploads are in progress.",
firstAction: "No",
secondAction: "Yes",
);
if (choice != DialogUserChoice.secondChoice) {
return;
}
}
await Configuration.instance.setShouldKeepDeviceAwake(value);
setState(() {});
},
),
),
sectionOptionSpacing,
]);
}
sectionOptions.addAll(
[
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Free up space",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -157,6 +119,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
captionedTextWidget: const CaptionedTextWidget(
title: "Deduplicate files",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {

View file

@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/account/delete_account_page.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@ -30,6 +31,7 @@ class DangerSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Logout",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
@ -41,6 +43,7 @@ class DangerSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Delete account",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {

View file

@ -6,6 +6,7 @@ import 'package:photos/core/configuration.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/theme/ente_theme.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.dart';
@ -32,6 +33,7 @@ class DebugSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Key attributes",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -43,6 +45,7 @@ class DebugSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Delete Local Import DB",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -55,6 +58,7 @@ class DebugSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Allow auto-upload for ignored files",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {

View file

@ -1,243 +0,0 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/ui/common/loading_widget.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/utils/data_util.dart';
class DetailsSectionWidget extends StatefulWidget {
const DetailsSectionWidget({Key? key}) : super(key: key);
@override
State<DetailsSectionWidget> createState() => _DetailsSectionWidgetState();
}
class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
late Image _background;
final _logger = Logger((_DetailsSectionWidgetState).toString());
@override
void initState() {
super.initState();
_background = const Image(
image: AssetImage("assets/storage_card_background.png"),
fit: BoxFit.fill,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// precache background image to avoid flicker
// https://stackoverflow.com/questions/51343735/flutter-image-preload
precacheImage(_background.image, context);
}
@override
Widget build(BuildContext context) {
final inheritedUserDetails = InheritedUserDetails.of(context);
if (inheritedUserDetails == null) {
_logger.severe(
(InheritedUserDetails).toString() +
' not found before ' +
(_DetailsSectionWidgetState).toString() +
' on tree',
);
throw Error();
} else {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return getSubscriptionPage();
},
),
);
},
child: containerForUserDetails(inheritedUserDetails),
);
}
}
Widget containerForUserDetails(
InheritedUserDetails inheritedUserDetails,
) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 428, maxHeight: 175),
child: Stack(
children: [
Container(
width: double.infinity,
color: Colors.transparent,
child: AspectRatio(
aspectRatio: 2 / 1,
child: _background,
),
),
FutureBuilder(
future: inheritedUserDetails.userDetails,
builder: (context, snapshot) {
if (snapshot.hasData) {
return userDetails(snapshot.data as UserDetails);
}
if (snapshot.hasError) {
_logger.severe('failed to load user details', snapshot.error);
return const EnteLoadingWidget();
}
return const EnteLoadingWidget();
},
),
const Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.chevron_right,
color: Colors.white,
size: 24,
),
),
],
),
);
}
Widget userDetails(UserDetails userDetails) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 20,
left: 16,
right: 16,
),
child: Container(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.topLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Storage",
style: Theme.of(context).textTheme.subtitle2!.copyWith(
color: Colors.white.withOpacity(0.7),
),
),
Text(
"${convertBytesToReadableFormat(userDetails.getFreeStorage())} of ${convertBytesToReadableFormat(userDetails.getTotalStorage())} free",
style: Theme.of(context)
.textTheme
.headline5!
.copyWith(color: Colors.white),
),
],
),
),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.2),
width: MediaQuery.of(context).size.width,
height: 4,
),
Container(
color: Colors.white.withOpacity(0.75),
width: MediaQuery.of(context).size.width *
((userDetails.getFamilyOrPersonalUsage()) /
userDetails.getTotalStorage()),
height: 4,
),
Container(
color: Colors.white,
width: MediaQuery.of(context).size.width *
(userDetails.usage / userDetails.getTotalStorage()),
height: 4,
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
userDetails.isPartOfFamily()
? Row(
children: [
Container(
width: 8.71,
height: 8.99,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"You",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
const Padding(
padding: EdgeInsets.only(right: 12),
),
Container(
width: 8.71,
height: 8.99,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.75),
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"Family",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
],
)
: Text(
"${convertBytesToReadableFormat(userDetails.getFamilyOrPersonalUsage())} used",
style:
Theme.of(context).textTheme.bodyText1!.copyWith(
color: Colors.white,
fontSize: 12,
),
),
],
),
],
)
],
),
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:photos/ente_theme_data.dart';
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/sessions_page.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
@ -72,8 +73,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
),
trailingSwitch: snapshot.hasData
? ToggleSwitchWidget(
value: snapshot.data,
onChanged: (value) async {
value: () => snapshot.data,
onChanged: () async {
final hasAuthenticated =
await LocalAuthenticationService.instance
.requestLocalAuthentication(
@ -81,7 +82,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
"Please authenticate to configure two-factor authentication",
);
if (hasAuthenticated) {
if (value) {
if (!snapshot.data) {
UserService.instance.setupTwoFactor(context);
} else {
_disableTwoFactor();
@ -105,18 +106,15 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
title: "Lockscreen",
),
trailingSwitch: ToggleSwitchWidget(
value: _config.shouldShowLockScreen(),
onChanged: (value) async {
final hasAuthenticated = await LocalAuthenticationService.instance
value: () => _config.shouldShowLockScreen(),
onChanged: () async {
await LocalAuthenticationService.instance
.requestLocalAuthForLockScreen(
context,
value,
!_config.shouldShowLockScreen(),
"Please authenticate to change lockscreen setting",
"To enable lockscreen, please setup device passcode or screen lock in your system settings.",
);
if (hasAuthenticated) {
setState(() {});
}
},
),
),
@ -130,81 +128,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
title: "Hide from recents",
),
trailingSwitch: ToggleSwitchWidget(
value: _config.shouldHideFromRecents(),
onChanged: (value) async {
if (value) {
final AlertDialog alert = AlertDialog(
title: const 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,
),
),
],
),
),
actions: [
TextButton(
child: Text(
"No",
style: TextStyle(
color:
Theme.of(context).colorScheme.defaultTextColor,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
},
),
TextButton(
child: Text(
"Yes",
style: TextStyle(
color:
Theme.of(context).colorScheme.defaultTextColor,
),
),
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(() {});
}
},
value: () => _config.shouldHideFromRecents(),
onChanged: _hideFromRecentsOnChanged,
),
),
sectionOptionSpacing,
@ -216,6 +141,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
captionedTextWidget: const CaptionedTextWidget(
title: "Active sessions",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -282,4 +208,74 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
},
);
}
Future<void> _hideFromRecentsOnChanged() async {
if (!_config.shouldHideFromRecents()) {
final AlertDialog alert = AlertDialog(
title: const 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,
),
),
],
),
),
actions: [
TextButton(
child: Text(
"No",
style: TextStyle(
color: Theme.of(context).colorScheme.defaultTextColor,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
TextButton(
child: Text(
"Yes",
style: TextStyle(
color: Theme.of(context).colorScheme.defaultTextColor,
),
),
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,
);
}
}
}

View file

@ -37,15 +37,13 @@ class SettingsTitleBarWidget extends StatelessWidget {
' on tree',
);
throw Error();
}
if (snapshot.hasData) {
} else if (snapshot.hasData) {
final userDetails = snapshot.data as UserDetails;
return Text(
"${NumberFormat().format(userDetails.fileCount)} memories",
style: getEnteTextTheme(context).largeBold,
);
}
if (snapshot.hasError) {
} else if (snapshot.hasError) {
logger.severe('failed to load user details');
return const EnteLoadingWidget();
} else {

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/theme/ente_theme.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.dart';
@ -61,6 +62,7 @@ class SocialsMenuItemWidget extends StatelessWidget {
captionedTextWidget: CaptionedTextWidget(
title: text,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {

View file

@ -0,0 +1,284 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/loading_widget.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/ui/settings/storage_error_widget.dart';
import 'package:photos/ui/settings/storage_progress_widget.dart';
import 'package:photos/utils/data_util.dart';
class StorageCardWidget extends StatefulWidget {
const StorageCardWidget({Key? key}) : super(key: key);
@override
State<StorageCardWidget> createState() => _StorageCardWidgetState();
}
class _StorageCardWidgetState extends State<StorageCardWidget> {
late Image _background;
final _logger = Logger((_StorageCardWidgetState).toString());
final ValueNotifier<bool> _isStorageCardPressed = ValueNotifier(false);
@override
void initState() {
super.initState();
_background = const Image(
image: AssetImage("assets/storage_card_background.png"),
fit: BoxFit.fill,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// precache background image to avoid flicker
// https://stackoverflow.com/questions/51343735/flutter-image-preload
precacheImage(_background.image, context);
}
@override
Widget build(BuildContext context) {
final inheritedUserDetails = InheritedUserDetails.of(context);
if (inheritedUserDetails == null) {
_logger.severe(
(InheritedUserDetails).toString() + 'is null',
);
throw Error();
} else {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return getSubscriptionPage();
},
),
);
},
onTapDown: (details) => _isStorageCardPressed.value = true,
onTapCancel: () => _isStorageCardPressed.value = false,
onTapUp: (details) => _isStorageCardPressed.value = false,
child: containerForUserDetails(inheritedUserDetails),
);
}
}
Widget containerForUserDetails(
InheritedUserDetails inheritedUserDetails,
) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 350),
child: AspectRatio(
aspectRatio: 2 / 1,
child: Stack(
children: [
_background,
FutureBuilder(
future: inheritedUserDetails.userDetails,
builder: (context, snapshot) {
if (snapshot.hasData) {
return userDetails(snapshot.data as UserDetails);
}
if (snapshot.hasError) {
_logger.severe(
'failed to load user details',
snapshot.error,
);
return const StorageErrorWidget();
}
return const EnteLoadingWidget(color: strokeBaseDark);
},
),
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 4),
child: ValueListenableBuilder<bool>(
builder: (BuildContext context, bool value, Widget? child) {
return Icon(
Icons.chevron_right_outlined,
color: value ? strokeMutedDark : strokeBaseDark,
);
},
valueListenable: _isStorageCardPressed,
),
),
),
],
),
),
);
}
Widget userDetails(UserDetails userDetails) {
const hundredMBinBytes = 107374182;
final isMobileScreenSmall = MediaQuery.of(context).size.width <= 365;
final freeSpaceInBytes = userDetails.getFreeStorage();
final shouldShowFreeSpaceInMBs = freeSpaceInBytes < hundredMBinBytes;
final usedSpaceInGB = roundBytesUsedToGBs(
userDetails.getFamilyOrPersonalUsage(),
userDetails.getFreeStorage(),
);
final totalStorageInGB =
convertBytesToGBs(userDetails.getTotalStorage()).truncate();
return Padding(
padding: EdgeInsets.fromLTRB(
16,
20,
16,
isMobileScreenSmall ? 12 : 20,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.topLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isMobileScreenSmall ? "Used space" : "Storage",
style: getEnteTextTheme(context)
.small
.copyWith(color: textMutedDark),
),
const SizedBox(height: 2),
RichText(
overflow: TextOverflow.ellipsis,
maxLines: 1,
text: TextSpan(
style: getEnteTextTheme(context)
.h3Bold
.copyWith(color: textBaseDark),
children: [
TextSpan(text: usedSpaceInGB.toString()),
TextSpan(text: isMobileScreenSmall ? "/" : " GB of "),
TextSpan(text: totalStorageInGB.toString() + " GB"),
TextSpan(text: isMobileScreenSmall ? "" : " used"),
],
),
),
],
),
),
Column(
children: [
Stack(
children: <Widget>[
const StorageProgressWidget(
color:
Color.fromRGBO(255, 255, 255, 0.2), //hardcoded in figma
fractionOfStorage: 1,
),
userDetails.isPartOfFamily()
? StorageProgressWidget(
color: strokeBaseDark,
fractionOfStorage:
((userDetails.getFamilyOrPersonalUsage()) /
userDetails.getTotalStorage()),
)
: const SizedBox.shrink(),
StorageProgressWidget(
color: userDetails.isPartOfFamily()
? getEnteColorScheme(context).primary300
: strokeBaseDark,
fractionOfStorage:
(userDetails.usage / userDetails.getTotalStorage()),
)
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
userDetails.isPartOfFamily()
? Row(
children: [
Container(
width: 8.71,
height: 8.99,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: getEnteColorScheme(context).primary300,
),
),
const SizedBox(width: 4),
Text(
"You",
style: getEnteTextTheme(context)
.miniBold
.copyWith(color: textBaseDark),
),
const SizedBox(width: 12),
Container(
width: 8.71,
height: 8.99,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: textBaseDark,
),
),
const SizedBox(width: 4),
Text(
"Family",
style: getEnteTextTheme(context)
.miniBold
.copyWith(color: textBaseDark),
),
],
)
: const SizedBox.shrink(),
RichText(
text: TextSpan(
style: getEnteTextTheme(context)
.mini
.copyWith(color: textFaintDark),
children: [
TextSpan(
text:
"${shouldShowFreeSpaceInMBs ? convertBytesToMBs(freeSpaceInBytes) : _roundedFreeSpace(totalStorageInGB, usedSpaceInGB)}",
),
TextSpan(
text: shouldShowFreeSpaceInMBs
? " MB free"
: " GB free",
)
],
),
),
],
),
],
)
],
),
);
}
num _roundedFreeSpace(num totalStorageInGB, num usedSpaceInGB) {
int fractionDigits;
//subtracting usedSpace from totalStorage in GB instead of converting from bytes so that free space and used space adds up in the UI
final freeSpace = totalStorageInGB - usedSpaceInGB;
//show one decimal place if free space is less than 10GB
if (freeSpace < 10) {
fractionDigits = 1;
} else {
fractionDigits = 0;
}
//omit decimal if decimal is 0
if (fractionDigits == 1 && freeSpace.remainder(1) == 0) {
fractionDigits = 0;
}
return num.parse(freeSpace.toStringAsFixed(fractionDigits));
}
}

View file

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
class StorageErrorWidget extends StatelessWidget {
const StorageErrorWidget({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(
Icons.error_outline_outlined,
color: strokeBaseDark,
),
const SizedBox(height: 8),
Text(
"Your storage details could not be fetched",
style: getEnteTextTheme(context).small.copyWith(
color: textMutedDark,
),
),
],
),
);
}
}

View file

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class StorageProgressWidget extends StatelessWidget {
final Color color;
final double fractionOfStorage;
const StorageProgressWidget({
required this.color,
required this.fractionOfStorage,
super.key,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constrains) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: color,
),
width: constrains.maxWidth * fractionOfStorage,
height: 4,
);
},
);
}
}

View file

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/web_page.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@ -34,6 +35,7 @@ class SupportSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Email",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
@ -45,6 +47,7 @@ class SupportSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Roadmap",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
@ -67,6 +70,7 @@ class SupportSectionWidget extends StatelessWidget {
captionedTextWidget: const CaptionedTextWidget(
title: "Report a bug",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {

View file

@ -4,6 +4,7 @@ import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/theme/ente_theme.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.dart';
@ -69,7 +70,8 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
title: toBeginningOfSentenceCase(themeMode.name),
textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
),
isHeaderOfExpansion: false,
pressedColor: getEnteColorScheme(context).fillFaint,
isExpandable: false,
trailingIcon: currentThemeMode == themeMode ? Icons.check : null,
onTap: () async {
AdaptiveTheme.of(context).setThemeMode(themeMode);

View file

@ -14,10 +14,10 @@ import 'package:photos/ui/settings/app_version_widget.dart';
import 'package:photos/ui/settings/backup_section_widget.dart';
import 'package:photos/ui/settings/danger_section_widget.dart';
import 'package:photos/ui/settings/debug_section_widget.dart';
import 'package:photos/ui/settings/details_section_widget.dart';
import 'package:photos/ui/settings/security_section_widget.dart';
import 'package:photos/ui/settings/settings_title_bar_widget.dart';
import 'package:photos/ui/settings/social_section_widget.dart';
import 'package:photos/ui/settings/storage_card_widget.dart';
import 'package:photos/ui/settings/support_section_widget.dart';
import 'package:photos/ui/settings/theme_switch_widget.dart';
@ -42,6 +42,7 @@ class SettingsPage extends StatelessWidget {
final List<Widget> contents = [];
contents.add(
Container(
constraints: const BoxConstraints(maxWidth: 350),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Align(
alignment: Alignment.centerLeft,
@ -65,7 +66,7 @@ class SettingsPage extends StatelessWidget {
contents.add(const SizedBox(height: 8));
if (hasLoggedIn) {
contents.addAll([
const DetailsSectionWidget(),
const StorageCardWidget(),
const SizedBox(height: 12),
const BackupSectionWidget(),
sectionSpacing,

View file

@ -126,7 +126,7 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
child: Column(
children: [
const SizedBox(height: 12),
const SectionTitle("Shared with me"),
const SectionTitle(title: "Shared with me"),
const SizedBox(height: 12),
collections.incoming.isNotEmpty
? Padding(
@ -150,7 +150,7 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
),
)
: _getIncomingCollectionEmptyState(),
const SectionTitle("Shared by me"),
const SectionTitle(title: "Shared by me"),
const SizedBox(height: 12),
collections.outgoing.isNotEmpty
? ListView.builder(

View file

@ -1,6 +1,7 @@
// @dart=2.9
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:extended_image/extended_image.dart';
@ -370,13 +371,19 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
existingFiles[0].creationTime,
))
.files;
// the index could be -1 if the files fetched doesn't contain the newly
// edited files
final selectionIndex =
files.indexWhere((file) => file.generatedID == newFile.generatedID);
if (selectionIndex == -1) {
files.add(newFile);
}
replacePage(
context,
DetailPage(
widget.detailPageConfig.copyWith(
files: files,
selectedIndex: files
.indexWhere((file) => file.generatedID == newFile.generatedID),
selectedIndex: min(selectionIndex, files.length - 1),
),
),
);

View file

@ -2,6 +2,7 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/ui/common/loading_widget.dart';
@ -11,6 +12,7 @@ import 'package:photos/utils/navigation_util.dart';
class CollectionsListOfFileWidget extends StatelessWidget {
final Future<Set<int>> allCollectionIDsOfFile;
const CollectionsListOfFileWidget(this.allCollectionIDsOfFile, {Key key})
: super(key: key);
@ -21,19 +23,23 @@ class CollectionsListOfFileWidget extends StatelessWidget {
builder: (context, snapshot) {
if (snapshot.hasData) {
final Set<int> collectionIDs = snapshot.data;
final collections = [];
final collections = <Collection>[];
for (var collectionID in collectionIDs) {
collections.add(
CollectionsService.instance.getCollectionByID(collectionID),
);
final c =
CollectionsService.instance.getCollectionByID(collectionID);
collections.add(c);
}
return ListView.builder(
itemCount: collections.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final bool isHidden = collections[index].isHidden();
return FileInfoCollectionWidget(
name: collections[index].name,
name: isHidden ? 'Hidden' : collections[index].name,
onTap: () {
if (isHidden) {
return;
}
routeToPage(
context,
CollectionPage(

View file

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:like_button/like_button.dart';
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension.dart';
import 'package:page_transition/page_transition.dart';
import 'package:path/path.dart' as file_path;
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/event_bus.dart';
@ -16,11 +17,15 @@ import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/ignored_file.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/models/trash_file.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/favorites_service.dart';
import 'package:photos/services/hidden_service.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/viewer/file/custom_app_bar.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/dialog_util.dart';
@ -99,11 +104,21 @@ class FadingAppBarState extends State<FadingAppBar> {
AppBar _buildAppBar() {
debugPrint("building app bar");
final List<Widget> actions = [];
final isTrashedFile = widget.file is TrashFile;
final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
final bool isOwnedByUser =
widget.file.ownerID == null || widget.file.ownerID == widget.userID;
bool isFileHidden = false;
if (isOwnedByUser && widget.file.uploadedFileID != null) {
isFileHidden = CollectionsService.instance
.getCollectionByID(widget.file.collectionID)
?.isHidden() ??
false;
}
// only show fav option for files owned by the user
if (widget.file.ownerID == null || widget.file.ownerID == widget.userID) {
if (isOwnedByUser && !isFileHidden) {
actions.add(_getFavoriteButton());
}
actions.add(
@ -132,8 +147,7 @@ class FadingAppBarState extends State<FadingAppBar> {
);
}
// options for files owned by the user
if (widget.file.ownerID == null ||
widget.file.ownerID == widget.userID) {
if (isOwnedByUser) {
items.add(
PopupMenuItem(
value: 2,
@ -169,12 +183,51 @@ class FadingAppBarState extends State<FadingAppBar> {
const Padding(
padding: EdgeInsets.all(8),
),
const Text("Use as"),
const Text("Set as"),
],
),
),
);
}
if (isOwnedByUser) {
if (!isFileHidden) {
items.add(
PopupMenuItem(
value: 4,
child: Row(
children: [
Icon(
Icons.visibility_off,
color: Theme.of(context).iconTheme.color,
),
const Padding(
padding: EdgeInsets.all(8),
),
const Text("Hide"),
],
),
),
);
} else {
items.add(
PopupMenuItem(
value: 5,
child: Row(
children: [
Icon(
Icons.visibility,
color: Theme.of(context).iconTheme.color,
),
const Padding(
padding: EdgeInsets.all(8),
),
const Text("Unhide"),
],
),
),
);
}
}
return items;
},
onSelected: (value) {
@ -184,6 +237,10 @@ class FadingAppBarState extends State<FadingAppBar> {
_showDeleteSheet(widget.file);
} else if (value == 3) {
_setAs(widget.file);
} else if (value == 4) {
_handleHideRequest(context);
} else if (value == 5) {
_handleUnHideRequest(context);
}
},
),
@ -197,6 +254,38 @@ class FadingAppBarState extends State<FadingAppBar> {
);
}
Future<void> _handleHideRequest(BuildContext context) async {
try {
final hideResult =
await CollectionsService.instance.hideFiles(context, [widget.file]);
if (hideResult) {
// delay to avoid black screen
await Future.delayed(const Duration(milliseconds: 300));
Navigator.of(context).pop();
}
} catch (e, s) {
_logger.severe("failed to update file visibility", e, s);
await showGenericErrorDialog(context);
}
}
Future<void> _handleUnHideRequest(BuildContext context) async {
final s = SelectedFiles();
s.files.add(widget.file);
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
s,
null,
actionType: CollectionActionType.unHide,
),
),
);
}
Widget _getFavoriteButton() {
return FutureBuilder(
future: FavoritesService.instance.isFavorite(widget.file),
@ -326,55 +415,97 @@ class FadingAppBarState extends State<FadingAppBar> {
Future<void> _download(File file) async {
final dialog = createProgressDialog(context, "Downloading...");
await dialog.show();
final FileType type = file.fileType;
// save and track image for livePhoto/image and video for FileType.video
final io.File fileToSave = await getFile(file);
final savedAsset = type == FileType.video
? (await PhotoManager.editor.saveVideo(fileToSave, title: file.title))
: (await PhotoManager.editor
.saveImageWithPath(fileToSave.path, title: file.title));
// immediately track assetID to avoid duplicate upload
await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
file.localID = savedAsset.id;
await FilesDB.instance.insert(file);
try {
final FileType type = file.fileType;
final bool downloadLivePhotoOnDroid =
type == FileType.livePhoto && Platform.isAndroid;
AssetEntity savedAsset;
final io.File fileToSave = await getFile(file);
if (type == FileType.image) {
savedAsset = await PhotoManager.editor
.saveImageWithPath(fileToSave.path, title: file.title);
} else if (type == FileType.video) {
savedAsset =
await PhotoManager.editor.saveVideo(fileToSave, title: file.title);
} else if (type == FileType.livePhoto) {
final io.File liveVideoFile =
await getFileFromServer(file, liveVideo: true);
if (liveVideoFile == null) {
throw AssertionError("Live video can not be null");
}
if (downloadLivePhotoOnDroid) {
await _saveLivePhotoOnDroid(fileToSave, liveVideoFile, file);
} else {
savedAsset = await PhotoManager.editor.darwin.saveLivePhoto(
imageFile: fileToSave,
videoFile: liveVideoFile,
title: file.title,
);
}
}
if (type == FileType.livePhoto) {
final io.File liveVideo = await getFileFromServer(file, liveVideo: true);
if (liveVideo == null) {
_logger.warning("Failed to find live video" + file.tag);
} else {
final videoTitle = file_path.basenameWithoutExtension(file.title) +
file_path.extension(liveVideo.path);
final savedAsset = (await PhotoManager.editor.saveVideo(
liveVideo,
title: videoTitle,
));
if (savedAsset != null) {
// immediately track assetID to avoid duplicate upload
await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
final ignoreVideoFile = IgnoredFile(
savedAsset.id,
savedAsset.title ?? videoTitle,
savedAsset.title ?? "",
savedAsset.relativePath ?? 'remoteDownload',
"remoteDownload",
);
debugPrint("IgnoreFile for auto-upload ${ignoreVideoFile.toString()}");
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
file.localID = savedAsset.id;
await FilesDB.instance.insert(file);
Bus.instance.fire(LocalPhotosUpdatedEvent([file]));
} else if (!downloadLivePhotoOnDroid && savedAsset == null) {
_logger.severe('Failed to save assert of type $type');
}
}
Bus.instance.fire(LocalPhotosUpdatedEvent([file]));
await dialog.hide();
if (file.fileType == FileType.livePhoto) {
showToast(context, "Photo and video saved to gallery");
} else {
showToast(context, "File saved to gallery");
await dialog.hide();
} catch (e) {
_logger.warning("Failed to save file", e);
await dialog.hide();
showGenericErrorDialog(context);
}
}
Future<void> _saveLivePhotoOnDroid(
io.File image,
io.File video,
File enteFile,
) async {
debugPrint("Downloading LivePhoto on Droid");
AssetEntity savedAsset = await PhotoManager.editor
.saveImageWithPath(image.path, title: enteFile.title);
IgnoredFile ignoreVideoFile = IgnoredFile(
savedAsset.id,
savedAsset.title ?? '',
savedAsset.relativePath ?? 'remoteDownload',
"remoteDownload",
);
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
final videoTitle = file_path.basenameWithoutExtension(enteFile.title) +
file_path.extension(video.path);
savedAsset = (await PhotoManager.editor.saveVideo(
video,
title: videoTitle,
));
ignoreVideoFile = IgnoredFile(
savedAsset.id,
savedAsset.title ?? videoTitle,
savedAsset.relativePath ?? 'remoteDownload',
"remoteDownload",
);
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
}
Future<void> _setAs(File file) async {
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();
try {
final io.File fileToSave = await getFile(file);
var m = MediaExtension();
final m = MediaExtension();
final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
if (result == false) {
showShortToast(context, "Something went wrong");

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:page_transition/page_transition.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/file.dart';
@ -11,6 +12,9 @@ import 'package:photos/models/file_type.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/models/trash_file.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/viewer/file/file_info_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
@ -72,8 +76,13 @@ class FadingBottomBarState extends State<FadingBottomBar> {
Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
color: Colors.white,
),
onPressed: () {
_displayInfo(widget.file);
onPressed: () async {
await _displayInfo(widget.file);
safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
await Future.delayed(
const Duration(milliseconds: 500),
); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
safeRefresh();
},
),
),
@ -82,6 +91,15 @@ class FadingBottomBarState extends State<FadingBottomBar> {
if (widget.file is TrashFile) {
_addTrashOptions(children);
}
final bool isUploadedByUser = widget.file.uploadedFileID != null &&
widget.file.ownerID == Configuration.instance.getUserID();
bool isFileHidden = false;
if (isUploadedByUser) {
isFileHidden = CollectionsService.instance
.getCollectionByID(widget.file.collectionID)
?.isHidden() ??
false;
}
if (!widget.showOnlyInfoButton && widget.file is! TrashFile) {
if (widget.file.fileType == FileType.image ||
widget.file.fileType == FileType.livePhoto) {
@ -103,20 +121,17 @@ class FadingBottomBarState extends State<FadingBottomBar> {
),
);
}
if (widget.file.uploadedFileID != null &&
widget.file.ownerID == Configuration.instance.getUserID()) {
if (isUploadedByUser && !isFileHidden) {
final bool isArchived =
widget.file.magicMetadata.visibility == visibilityArchive;
children.add(
Tooltip(
message: isArchived ? "Unhide" : "Hide",
message: isArchived ? "Unarchive" : "Archive",
child: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12),
child: IconButton(
icon: Icon(
isArchived
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
isArchived ? Icons.unarchive : Icons.archive_outlined,
color: Colors.white,
),
onPressed: () async {
@ -176,9 +191,31 @@ class FadingBottomBarState extends State<FadingBottomBar> {
),
child: Padding(
padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
widget.file.caption?.isNotEmpty ?? false
? Padding(
padding: const EdgeInsets.fromLTRB(
16,
28,
16,
12,
),
child: Text(
widget.file.caption,
style: getEnteTextTheme(context)
.small
.copyWith(color: textBaseDark),
textAlign: TextAlign.center,
),
)
: const SizedBox.shrink(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
),
],
),
),
),
@ -242,11 +279,19 @@ class FadingBottomBarState extends State<FadingBottomBar> {
}
Future<void> _displayInfo(File file) async {
return showModalBottomSheet<void>(
final colorScheme = getEnteColorScheme(context);
return showBarModalBottomSheet(
topControl: const SizedBox.shrink(),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
backgroundColor: colorScheme.backgroundBase,
barrierColor: backdropFaintDark,
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return FileInfoWidget(file);
return Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: FileInfoWidget(file),
);
},
);
}

View file

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:photos/models/file.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/utils/magic_util.dart';
class FileCaptionWidget extends StatefulWidget {
final File file;
const FileCaptionWidget({required this.file, super.key});
@override
State<FileCaptionWidget> createState() => _FileCaptionWidgetState();
}
class _FileCaptionWidgetState extends State<FileCaptionWidget> {
int maxLength = 280;
int currentLength = 0;
final _textController = TextEditingController();
final _focusNode = FocusNode();
String? editedCaption;
String? hintText = "Add a description...";
@override
void initState() {
_focusNode.addListener(() {
final caption = widget.file.caption;
if (_focusNode.hasFocus && caption != null) {
_textController.text = caption;
editedCaption = caption;
}
});
editedCaption = widget.file.caption;
if (editedCaption != null && editedCaption!.isNotEmpty) {
hintText = editedCaption;
}
super.initState();
}
@override
void dispose() {
if (editedCaption != null) {
editFileCaption(null, widget.file, editedCaption);
}
_textController.dispose();
_focusNode.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return TextField(
onEditingComplete: () async {
if (editedCaption != null) {
await editFileCaption(context, widget.file, editedCaption);
if (mounted) {
setState(() {});
}
}
_focusNode.unfocus();
},
controller: _textController,
focusNode: _focusNode,
decoration: InputDecoration(
counterStyle: textTheme.mini.copyWith(color: colorScheme.textMuted),
counterText: currentLength > 99
? currentLength.toString() + " / " + maxLength.toString()
: "",
contentPadding: const EdgeInsets.all(16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(2),
borderSide: const BorderSide(
width: 0,
style: BorderStyle.none,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(2),
borderSide: const BorderSide(
width: 0,
style: BorderStyle.none,
),
),
filled: true,
fillColor: colorScheme.fillFaint,
hintText: hintText,
hintStyle: getEnteTextTheme(context)
.small
.copyWith(color: colorScheme.textMuted),
),
style: getEnteTextTheme(context).small,
cursorWidth: 1.5,
maxLength: maxLength,
minLines: 1,
maxLines: 6,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.text,
onChanged: (value) {
setState(() {
hintText = "Add a description...";
currentLength = value.length;
editedCaption = value;
});
},
);
}
}

View file

@ -9,10 +9,13 @@ import 'package:photos/db/files_db.dart';
import "package:photos/ente_theme_data.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_type.dart";
import 'package:photos/ui/common/DividerWithPadding.dart';
import 'package:photos/ui/components/divider_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import 'package:photos/ui/viewer/file/collections_list_of_file_widget.dart';
import 'package:photos/ui/viewer/file/device_folders_list_of_file_widget.dart';
import 'package:photos/ui/viewer/file/raw_exif_button.dart';
import 'package:photos/ui/viewer/file/file_caption_widget.dart';
import 'package:photos/ui/viewer/file/raw_exif_list_tile_widget.dart';
import "package:photos/utils/date_time_util.dart";
import "package:photos/utils/exif_util.dart";
import "package:photos/utils/file_util.dart";
@ -51,9 +54,11 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
widget.file.fileType == FileType.livePhoto;
if (_isImage) {
getExif(widget.file).then((exif) {
setState(() {
_exif = exif;
});
if (mounted) {
setState(() {
_exif = exif;
});
}
});
}
super.initState();
@ -88,9 +93,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
final bool showDimension =
_exifData["resolution"] != null && _exifData["megaPixels"] != null;
final listTiles = <Widget>[
widget.file.uploadedFileID == null ||
Configuration.instance.getUserID() != file.ownerID
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: FileCaptionWidget(file: widget.file),
),
ListTile(
horizontalTitleGap: 2,
leading: const Padding(
padding: EdgeInsets.only(top: 8, left: 6),
padding: EdgeInsets.only(top: 8),
child: Icon(Icons.calendar_today_rounded),
),
title: Text(
@ -119,17 +132,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
)
: const SizedBox.shrink(),
),
const DividerWithPadding(left: 70, right: 20),
ListTile(
horizontalTitleGap: 2,
leading: _isImage
? const Padding(
padding: EdgeInsets.only(top: 8, left: 6),
padding: EdgeInsets.only(top: 8),
child: Icon(
Icons.image,
),
)
: const Padding(
padding: EdgeInsets.only(top: 8, left: 6),
padding: EdgeInsets.only(top: 8),
child: Icon(
Icons.video_camera_back,
size: 27,
@ -167,13 +180,10 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
icon: const Icon(Icons.edit),
),
),
const DividerWithPadding(left: 70, right: 20),
showExifListTile
? ListTile(
leading: const Padding(
padding: EdgeInsets.only(left: 6),
child: Icon(Icons.camera_rounded),
),
horizontalTitleGap: 2,
leading: const Icon(Icons.camera_rounded),
title: Text(_exifData["takenOnDevice"] ?? "--"),
subtitle: Row(
children: [
@ -205,27 +215,22 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
],
),
)
: const SizedBox.shrink(),
showExifListTile
? const DividerWithPadding(left: 70, right: 20)
: const SizedBox.shrink(),
: null,
SizedBox(
height: 62,
child: ListTile(
leading: const Padding(
padding: EdgeInsets.only(left: 6),
child: Icon(Icons.folder_outlined),
),
horizontalTitleGap: 0,
leading: const Icon(Icons.folder_outlined),
title: fileIsBackedup
? CollectionsListOfFileWidget(allCollectionIDsOfFile)
: DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
),
),
const DividerWithPadding(left: 70, right: 20),
(file.uploadedFileID != null && file.updationTime != null)
? ListTile(
horizontalTitleGap: 2,
leading: const Padding(
padding: EdgeInsets.only(top: 8, left: 6),
padding: EdgeInsets.only(top: 8),
child: Icon(Icons.cloud_upload_outlined),
),
title: Text(
@ -245,48 +250,53 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
),
),
)
: const SizedBox.shrink(),
_isImage
? Padding(
padding: const EdgeInsets.fromLTRB(0, 24, 0, 16),
child: SafeArea(
child: RawExifButton(_exif, widget.file),
),
)
: const SizedBox(
height: 12,
)
: null,
_isImage ? RawExifListTileWidget(_exif, widget.file) : null,
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(
Icons.close,
listTiles.removeWhere(
(element) => element == null,
);
return SafeArea(
top: false,
child: Scrollbar(
thickness: 4,
radius: const Radius.circular(2),
thumbVisibility: true,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CustomScrollView(
shrinkWrap: true,
slivers: <Widget>[
TitleBarWidget(
isFlexibleSpaceDisabled: true,
title: "Details",
isOnTopOfScreen: false,
leading: IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.primary,
onTap: () => Navigator.pop(context),
),
),
const SizedBox(width: 6),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
"Details",
style: Theme.of(context).textTheme.bodyText1,
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index.isOdd) {
return index == 1
? const SizedBox.shrink()
: const DividerWidget(dividerType: DividerType.menu);
} else {
return listTiles[index ~/ 2];
}
},
childCount: (listTiles.length * 2) - 1,
),
),
)
],
),
),
...listTiles
],
),
);
}

View file

@ -1,100 +0,0 @@
// @dart=2.9
import 'package:exif/exif.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import "package:photos/models/file.dart";
import 'package:photos/ui/viewer/file/exif_info_dialog.dart';
import 'package:photos/utils/toast_util.dart';
enum Status {
loading,
exifIsAvailable,
noExif,
}
class RawExifButton extends StatelessWidget {
final File file;
final Map<String, IfdTag> exif;
const RawExifButton(this.exif, this.file, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
Status exifStatus = Status.loading;
if (exif == null) {
exifStatus = Status.loading;
} else if (exif.isNotEmpty) {
exifStatus = Status.exifIsAvailable;
} else {
exifStatus = Status.noExif;
}
return GestureDetector(
onTap:
exifStatus == Status.loading || exifStatus == Status.exifIsAvailable
? () {
showDialog(
context: context,
builder: (BuildContext context) {
return ExifInfoDialog(file);
},
barrierColor: Colors.black87,
);
}
: exifStatus == Status.noExif
? () {
showShortToast(context, "This image has no exif data");
}
: null,
child: Container(
height: 40,
width: 140,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.inverseBackgroundColor
.withOpacity(0.12),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Center(
child: exifStatus == Status.loading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CupertinoActivityIndicator(
radius: 8,
),
SizedBox(
width: 8,
),
Text('EXIF')
],
)
: exifStatus == Status.exifIsAvailable
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.feed_outlined),
SizedBox(
width: 8,
),
Text('Raw EXIF'),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.feed_outlined),
SizedBox(
width: 8,
),
Text('No EXIF'),
],
),
),
),
);
}
}

View file

@ -0,0 +1,71 @@
// @dart=2.9
import 'package:exif/exif.dart';
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import "package:photos/models/file.dart";
import 'package:photos/ui/viewer/file/exif_info_dialog.dart';
import 'package:photos/utils/toast_util.dart';
enum Status {
loading,
exifIsAvailable,
noExif,
}
class RawExifListTileWidget extends StatelessWidget {
final File file;
final Map<String, IfdTag> exif;
const RawExifListTileWidget(this.exif, this.file, {Key key})
: super(key: key);
@override
Widget build(BuildContext context) {
Status exifStatus = Status.loading;
if (exif == null) {
exifStatus = Status.loading;
} else if (exif.isNotEmpty) {
exifStatus = Status.exifIsAvailable;
} else {
exifStatus = Status.noExif;
}
return GestureDetector(
onTap: exifStatus == Status.exifIsAvailable
? () {
showDialog(
context: context,
builder: (BuildContext context) {
return ExifInfoDialog(file);
},
barrierColor: Colors.black87,
);
}
: exifStatus == Status.noExif
? () {
showShortToast(context, "This image has no exif data");
}
: null,
child: ListTile(
horizontalTitleGap: 2,
leading: const Padding(
padding: EdgeInsets.only(top: 8),
child: Icon(Icons.feed_outlined),
),
title: const Text("EXIF"),
subtitle: Text(
exifStatus == Status.loading
? "Loading EXIF data.."
: exifStatus == Status.exifIsAvailable
? "View all EXIF data"
: "No EXIF data",
style: Theme.of(context).textTheme.bodyText2.copyWith(
color: Theme.of(context)
.colorScheme
.defaultTextColor
.withOpacity(0.5),
),
),
),
);
}
}

View file

@ -78,7 +78,9 @@ class _VideoWidgetState extends State<VideoWidget> {
.getFileSize(widget.file.uploadedFileID)
.then((value) {
widget.file.fileSize = value;
setState(() {});
if (mounted) {
setState(() {});
}
});
}
}

View file

@ -66,7 +66,7 @@ class ArchivePage extends StatelessWidget {
preferredSize: const Size.fromHeight(50.0),
child: GalleryAppBarWidget(
appBarType,
"Hidden",
"Archive",
_selectedFiles,
),
),

View file

@ -10,6 +10,7 @@ import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/ui/viewer/gallery/empty_state.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart';
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
@ -19,16 +20,21 @@ class CollectionPage extends StatelessWidget {
final String tagPrefix;
final GalleryType appBarType;
final _selectedFiles = SelectedFiles();
bool hasVerifiedLock;
CollectionPage(
this.c, {
this.tagPrefix = "collection",
this.appBarType = GalleryType.ownedCollection,
this.hasVerifiedLock = false,
Key key,
}) : super(key: key);
@override
Widget build(Object context) {
if (hasVerifiedLock == false && c.collection.isHidden()) {
return const EmptyState();
}
final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
@ -55,6 +61,7 @@ class CollectionPage extends StatelessWidget {
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
EventType.hide,
},
tagPrefix: tagPrefix,
selectedFiles: _selectedFiles,

Some files were not shown because too many files have changed in this diff Show more