Merge branch 'master' into empty_albums
This commit is contained in:
commit
bbbc61fd4e
118 changed files with 3632 additions and 1671 deletions
33
.github/workflows/code_quality.yml
vendored
Normal file
33
.github/workflows/code_quality.yml
vendored
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -20,4 +20,6 @@ enum EventType {
|
|||
deletedFromEverywhere,
|
||||
archived,
|
||||
unarchived,
|
||||
hide,
|
||||
unhide,
|
||||
}
|
||||
|
|
|
@ -16,3 +16,9 @@ enum TabChangedEventSource {
|
|||
collectionsPage,
|
||||
backButton,
|
||||
}
|
||||
|
||||
class TabDoubleTapEvent extends Event {
|
||||
final int selectedIndex;
|
||||
|
||||
TabDoubleTapEvent(this.selectedIndex);
|
||||
}
|
||||
|
|
57
lib/models/api/collection/create_request.dart
Normal file
57
lib/models/api/collection/create_request.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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":
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
enum GalleryType {
|
||||
homepage,
|
||||
archive,
|
||||
hidden,
|
||||
trash,
|
||||
localFolder,
|
||||
// indicator for gallery view of collections shared with the user
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@ enum ResultType {
|
|||
year,
|
||||
fileType,
|
||||
fileExtension,
|
||||
fileCaption,
|
||||
event
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
139
lib/services/hidden_service.dart
Normal file
139
lib/services/hidden_service.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 don’t 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 don’t 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) {
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
143
lib/ui/backup_settings_screen.dart
Normal file
143
lib/ui/backup_settings_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
104
lib/ui/collections/archived_collections_button_widget.dart
Normal file
104
lib/ui/collections/archived_collections_button_widget.dart
Normal 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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
59
lib/ui/components/divider_widget.dart
Normal file
59
lib/ui/components/divider_widget.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
108
lib/ui/components/icon_button_widget.dart
Normal file
108
lib/ui/components/icon_button_widget.dart
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
20
lib/ui/components/menu_section_description_widget.dart
Normal file
20
lib/ui/components/menu_section_description_widget.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
55
lib/ui/components/title_bar_title_widget.dart
Normal file
55
lib/ui/components/title_bar_title_widget.dart
Normal 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();
|
||||
}
|
||||
}
|
149
lib/ui/components/title_bar_widget.dart
Normal file
149
lib/ui/components/title_bar_widget.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
26
lib/ui/home/header_widget.dart
Normal file
26
lib/ui/home/header_widget.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
189
lib/ui/home/home_bottom_nav_bar.dart
Normal file
189
lib/ui/home/home_bottom_nav_bar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
88
lib/ui/home/home_gallery_widget.dart
Normal file
88
lib/ui/home/home_gallery_widget.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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) {
|
63
lib/ui/home/start_backup_hook_widget.dart
Normal file
63
lib/ui/home/start_backup_hook_widget.dart
Normal 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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -368,7 +368,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
planWidgets.add(
|
||||
SubscriptionPlanWidget(
|
||||
storage: _freePlan.storage,
|
||||
price: "free",
|
||||
price: "Free trial",
|
||||
period: "",
|
||||
isActive: true,
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: () {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: () {
|
||||
|
|
284
lib/ui/settings/storage_card_widget.dart
Normal file
284
lib/ui/settings/storage_card_widget.dart
Normal 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));
|
||||
}
|
||||
}
|
31
lib/ui/settings/storage_error_widget.dart
Normal file
31
lib/ui/settings/storage_error_widget.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
27
lib/ui/settings/storage_progress_widget.dart
Normal file
27
lib/ui/settings/storage_progress_widget.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
107
lib/ui/viewer/file/file_caption_widget.dart
Normal file
107
lib/ui/viewer/file/file_caption_widget.dart
Normal 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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
71
lib/ui/viewer/file/raw_exif_list_tile_widget.dart
Normal file
71
lib/ui/viewer/file/raw_exif_list_tile_widget.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -78,7 +78,9 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||
.getFileSize(widget.file.uploadedFileID)
|
||||
.then((value) {
|
||||
widget.file.fileSize = value;
|
||||
setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class ArchivePage extends StatelessWidget {
|
|||
preferredSize: const Size.fromHeight(50.0),
|
||||
child: GalleryAppBarWidget(
|
||||
appBarType,
|
||||
"Hidden",
|
||||
"Archive",
|
||||
_selectedFiles,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue