diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml new file mode 100644 index 000000000..4c02b8d61 --- /dev/null +++ b/.github/workflows/crowdin.yml @@ -0,0 +1,33 @@ +name: Sync crowdin translation + +on: + workflow_dispatch: + push: + paths: + - 'lib/l10n/app_en.arb' + branches: [ main ] + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: crowdin action + uses: crowdin/github-action@v1 + with: + upload_sources: true + upload_translations: true + download_translations: true + localization_branch_name: l10n_translations + create_pull_request: true + skip_untranslated_strings: true + pull_request_title: 'New Translations' + pull_request_body: 'New translations via [Crowdin GH Action](https://github.com/crowdin/github-action)' + pull_request_base_branch_name: 'main' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/README.md b/README.md index 2ee7a5ae3..544188b73 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ You can alternatively install the build from PlayStore or F-Droid. 3. Pull in all submodules with `git submodule update --init --recursive` 4. Enable repo git hooks `git config core.hooksPath hooks` 5. Setup TensorFlowLite by executing `setup.sh` -6. For Android, run `flutter build apk --release --flavor independent` +6. For Android, [setup your keystore](https://docs.flutter.dev/deployment/android#create-an-upload-keystore) and run `flutter build apk --release --flavor independent` 7. For iOS, run `flutter build ios`
diff --git a/assets/models/labelmap.txt b/assets/models/cocossd/labels.txt similarity index 100% rename from assets/models/labelmap.txt rename to assets/models/cocossd/labels.txt diff --git a/assets/models/detect.tflite b/assets/models/cocossd/model.tflite similarity index 100% rename from assets/models/detect.tflite rename to assets/models/cocossd/model.tflite diff --git a/assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt b/assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt new file mode 100644 index 000000000..fe811239d --- /dev/null +++ b/assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt @@ -0,0 +1,1001 @@ +background +tench +goldfish +great white shark +tiger shark +hammerhead +electric ray +stingray +cock +hen +ostrich +brambling +goldfinch +house finch +junco +indigo bunting +robin +bulbul +jay +magpie +chickadee +water ouzel +kite +bald eagle +vulture +great grey owl +European fire salamander +common newt +eft +spotted salamander +axolotl +bullfrog +tree frog +tailed frog +loggerhead +leatherback turtle +mud turtle +terrapin +box turtle +banded gecko +common iguana +American chameleon +whiptail +agama +frilled lizard +alligator lizard +Gila monster +green lizard +African chameleon +Komodo dragon +African crocodile +American alligator +triceratops +thunder snake +ringneck snake +hognose snake +green snake +king snake +garter snake +water snake +vine snake +night snake +boa constrictor +rock python +Indian cobra +green mamba +sea snake +horned viper +diamondback +sidewinder +trilobite +harvestman +scorpion +black and gold garden spider +barn spider +garden spider +black widow +tarantula +wolf spider +tick +centipede +black grouse +ptarmigan +ruffed grouse +prairie chicken +peacock +quail +partridge +African grey +macaw +sulphur-crested cockatoo +lorikeet +coucal +bee eater +hornbill +hummingbird +jacamar +toucan +drake +red-breasted merganser +goose +black swan +tusker +echidna +platypus +wallaby +koala +wombat +jellyfish +sea anemone +brain coral +flatworm +nematode +conch +snail +slug +sea slug +chiton +chambered nautilus +Dungeness crab +rock crab +fiddler crab +king crab +American lobster +spiny lobster +crayfish +hermit crab +isopod +white stork +black stork +spoonbill +flamingo +little blue heron +American egret +bittern +crane +limpkin +European gallinule +American coot +bustard +ruddy turnstone +red-backed sandpiper +redshank +dowitcher +oystercatcher +pelican +king penguin +albatross +grey whale +killer whale +dugong +sea lion +Chihuahua +Japanese spaniel +Maltese dog +Pekinese +Shih-Tzu +Blenheim spaniel +papillon +toy terrier +Rhodesian ridgeback +Afghan hound +basset +beagle +bloodhound +bluetick +black-and-tan coonhound +Walker hound +English foxhound +redbone +borzoi +Irish wolfhound +Italian greyhound +whippet +Ibizan hound +Norwegian elkhound +otterhound +Saluki +Scottish deerhound +Weimaraner +Staffordshire bullterrier +American Staffordshire terrier +Bedlington terrier +Border terrier +Kerry blue terrier +Irish terrier +Norfolk terrier +Norwich terrier +Yorkshire terrier +wire-haired fox terrier +Lakeland terrier +Sealyham terrier +Airedale +cairn +Australian terrier +Dandie Dinmont +Boston bull +miniature schnauzer +giant schnauzer +standard schnauzer +Scotch terrier +Tibetan terrier +silky terrier +soft-coated wheaten terrier +West Highland white terrier +Lhasa +flat-coated retriever +curly-coated retriever +golden retriever +Labrador retriever +Chesapeake Bay retriever +German short-haired pointer +vizsla +English setter +Irish setter +Gordon setter +Brittany spaniel +clumber +English springer +Welsh springer spaniel +cocker spaniel +Sussex spaniel +Irish water spaniel +kuvasz +schipperke +groenendael +malinois +briard +kelpie +komondor +Old English sheepdog +Shetland sheepdog +collie +Border collie +Bouvier des Flandres +Rottweiler +German shepherd +Doberman +miniature pinscher +Greater Swiss Mountain dog +Bernese mountain dog +Appenzeller +EntleBucher +boxer +bull mastiff +Tibetan mastiff +French bulldog +Great Dane +Saint Bernard +Eskimo dog +malamute +Siberian husky +dalmatian +affenpinscher +basenji +pug +Leonberg +Newfoundland +Great Pyrenees +Samoyed +Pomeranian +chow +keeshond +Brabancon griffon +Pembroke +Cardigan +toy poodle +miniature poodle +standard poodle +Mexican hairless +timber wolf +white wolf +red wolf +coyote +dingo +dhole +African hunting dog +hyena +red fox +kit fox +Arctic fox +grey fox +tabby +tiger cat +Persian cat +Siamese cat +Egyptian cat +cougar +lynx +leopard +snow leopard +jaguar +lion +tiger +cheetah +brown bear +American black bear +ice bear +sloth bear +mongoose +meerkat +tiger beetle +ladybug +ground beetle +long-horned beetle +leaf beetle +dung beetle +rhinoceros beetle +weevil +fly +bee +ant +grasshopper +cricket +walking stick +cockroach +mantis +cicada +leafhopper +lacewing +dragonfly +damselfly +admiral +ringlet +monarch +cabbage butterfly +sulphur butterfly +lycaenid +starfish +sea urchin +sea cucumber +wood rabbit +hare +Angora +hamster +porcupine +fox squirrel +marmot +beaver +guinea pig +sorrel +zebra +hog +wild boar +warthog +hippopotamus +ox +water buffalo +bison +ram +bighorn +ibex +hartebeest +impala +gazelle +Arabian camel +llama +weasel +mink +polecat +black-footed ferret +otter +skunk +badger +armadillo +three-toed sloth +orangutan +gorilla +chimpanzee +gibbon +siamang +guenon +patas +baboon +macaque +langur +colobus +proboscis monkey +marmoset +capuchin +howler monkey +titi +spider monkey +squirrel monkey +Madagascar cat +indri +Indian elephant +African elephant +lesser panda +giant panda +barracouta +eel +coho +rock beauty +anemone fish +sturgeon +gar +lionfish +puffer +abacus +abaya +academic gown +accordion +acoustic guitar +aircraft carrier +airliner +airship +altar +ambulance +amphibian +analog clock +apiary +apron +ashcan +assault rifle +backpack +bakery +balance beam +balloon +ballpoint +Band Aid +banjo +bannister +barbell +barber chair +barbershop +barn +barometer +barrel +barrow +baseball +basketball +bassinet +bassoon +bathing cap +bath towel +bathtub +beach wagon +beacon +beaker +bearskin +beer bottle +beer glass +bell cote +bib +bicycle-built-for-two +bikini +binder +binoculars +birdhouse +boathouse +bobsled +bolo tie +bonnet +bookcase +bookshop +bottlecap +bow +bow tie +brass +brassiere +breakwater +breastplate +broom +bucket +buckle +bulletproof vest +bullet train +butcher shop +cab +caldron +candle +cannon +canoe +can opener +cardigan +car mirror +carousel +carpenter's kit +carton +car wheel +cash machine +cassette +cassette player +castle +catamaran +CD player +cello +cellular telephone +chain +chainlink fence +chain mail +chain saw +chest +chiffonier +chime +china cabinet +Christmas stocking +church +cinema +cleaver +cliff dwelling +cloak +clog +cocktail shaker +coffee mug +coffeepot +coil +combination lock +computer keyboard +confectionery +container ship +convertible +corkscrew +cornet +cowboy boot +cowboy hat +cradle +crane +crash helmet +crate +crib +Crock Pot +croquet ball +crutch +cuirass +dam +desk +desktop computer +dial telephone +diaper +digital clock +digital watch +dining table +dishrag +dishwasher +disk brake +dock +dogsled +dome +doormat +drilling platform +drum +drumstick +dumbbell +Dutch oven +electric fan +electric guitar +electric locomotive +entertainment center +envelope +espresso maker +face powder +feather boa +file +fireboat +fire engine +fire screen +flagpole +flute +folding chair +football helmet +forklift +fountain +fountain pen +four-poster +freight car +French horn +frying pan +fur coat +garbage truck +gasmask +gas pump +goblet +go-kart +golf ball +golfcart +gondola +gong +gown +grand piano +greenhouse +grille +grocery store +guillotine +hair slide +hair spray +half track +hammer +hamper +hand blower +hand-held computer +handkerchief +hard disc +harmonica +harp +harvester +hatchet +holster +home theater +honeycomb +hook +hoopskirt +horizontal bar +horse cart +hourglass +iPod +iron +jack-o'-lantern +jean +jeep +jersey +jigsaw puzzle +jinrikisha +joystick +kimono +knee pad +knot +lab coat +ladle +lampshade +laptop +lawn mower +lens cap +letter opener +library +lifeboat +lighter +limousine +liner +lipstick +Loafer +lotion +loudspeaker +loupe +lumbermill +magnetic compass +mailbag +mailbox +maillot +maillot +manhole cover +maraca +marimba +mask +matchstick +maypole +maze +measuring cup +medicine chest +megalith +microphone +microwave +military uniform +milk can +minibus +miniskirt +minivan +missile +mitten +mixing bowl +mobile home +Model T +modem +monastery +monitor +moped +mortar +mortarboard +mosque +mosquito net +motor scooter +mountain bike +mountain tent +mouse +mousetrap +moving van +muzzle +nail +neck brace +necklace +nipple +notebook +obelisk +oboe +ocarina +odometer +oil filter +organ +oscilloscope +overskirt +oxcart +oxygen mask +packet +paddle +paddlewheel +padlock +paintbrush +pajama +palace +panpipe +paper towel +parachute +parallel bars +park bench +parking meter +passenger car +patio +pay-phone +pedestal +pencil box +pencil sharpener +perfume +Petri dish +photocopier +pick +pickelhaube +picket fence +pickup +pier +piggy bank +pill bottle +pillow +ping-pong ball +pinwheel +pirate +pitcher +plane +planetarium +plastic bag +plate rack +plow +plunger +Polaroid camera +pole +police van +poncho +pool table +pop bottle +pot +potter's wheel +power drill +prayer rug +printer +prison +projectile +projector +puck +punching bag +purse +quill +quilt +racer +racket +radiator +radio +radio telescope +rain barrel +recreational vehicle +reel +reflex camera +refrigerator +remote control +restaurant +revolver +rifle +rocking chair +rotisserie +rubber eraser +rugby ball +rule +running shoe +safe +safety pin +saltshaker +sandal +sarong +sax +scabbard +scale +school bus +schooner +scoreboard +screen +screw +screwdriver +seat belt +sewing machine +shield +shoe shop +shoji +shopping basket +shopping cart +shovel +shower cap +shower curtain +ski +ski mask +sleeping bag +slide rule +sliding door +slot +snorkel +snowmobile +snowplow +soap dispenser +soccer ball +sock +solar dish +sombrero +soup bowl +space bar +space heater +space shuttle +spatula +speedboat +spider web +spindle +sports car +spotlight +stage +steam locomotive +steel arch bridge +steel drum +stethoscope +stole +stone wall +stopwatch +stove +strainer +streetcar +stretcher +studio couch +stupa +submarine +suit +sundial +sunglass +sunglasses +sunscreen +suspension bridge +swab +sweatshirt +swimming trunks +swing +switch +syringe +table lamp +tank +tape player +teapot +teddy +television +tennis ball +thatch +theater curtain +thimble +thresher +throne +tile roof +toaster +tobacco shop +toilet seat +torch +totem pole +tow truck +toyshop +tractor +trailer truck +tray +trench coat +tricycle +trimaran +tripod +triumphal arch +trolleybus +trombone +tub +turnstile +typewriter keyboard +umbrella +unicycle +upright +vacuum +vase +vault +velvet +vending machine +vestment +viaduct +violin +volleyball +waffle iron +wall clock +wallet +wardrobe +warplane +washbasin +washer +water bottle +water jug +water tower +whiskey jug +whistle +wig +window screen +window shade +Windsor tie +wine bottle +wing +wok +wooden spoon +wool +worm fence +wreck +yawl +yurt +web site +comic book +crossword puzzle +street sign +traffic light +book jacket +menu +plate +guacamole +consomme +hot pot +trifle +ice cream +ice lolly +French loaf +bagel +pretzel +cheeseburger +hotdog +mashed potato +head cabbage +broccoli +cauliflower +zucchini +spaghetti squash +acorn squash +butternut squash +cucumber +artichoke +bell pepper +cardoon +mushroom +Granny Smith +strawberry +orange +lemon +fig +pineapple +banana +jackfruit +custard apple +pomegranate +hay +carbonara +chocolate sauce +dough +meat loaf +pizza +potpie +burrito +red wine +espresso +cup +eggnog +alp +bubble +cliff +coral reef +geyser +lakeside +promontory +sandbar +seashore +valley +volcano +ballplayer +groom +scuba diver +rapeseed +daisy +yellow lady's slipper +corn +acorn +hip +buckeye +coral fungus +agaric +gyromitra +stinkhorn +earthstar +hen-of-the-woods +bolete +ear +toilet tissue diff --git a/assets/models/mobilenet/mobilenet_v1_1.0_224_quant.tflite b/assets/models/mobilenet/mobilenet_v1_1.0_224_quant.tflite new file mode 100644 index 000000000..437640b06 Binary files /dev/null and b/assets/models/mobilenet/mobilenet_v1_1.0_224_quant.tflite differ diff --git a/assets/models/scenes/labels.txt b/assets/models/scenes/labels.txt new file mode 100644 index 000000000..cb284a62e --- /dev/null +++ b/assets/models/scenes/labels.txt @@ -0,0 +1,30 @@ +waterfall +snow +landscape +underwater +architecture +sunset / sunrise +blue sky +cloudy sky +greenery +autumn leaves +potrait +flower +night shot +stage concert +fireworks +candle light +neon lights +indoor +backlight +text documents +qr images +group potrait +computer screens +kids +dog +cat +macro +food +beach +mountain diff --git a/assets/models/scenes/model.tflite b/assets/models/scenes/model.tflite new file mode 100644 index 000000000..f2c942354 Binary files /dev/null and b/assets/models/scenes/model.tflite differ diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..5ad30f6d0 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,6 @@ +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_PERSONAL_TOKEN + +files: + - source: /lib/l10n/app_en.arb + translation: /lib/l10n/app_%two_letters_code%.arb diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 13a4c91ed..925453e86 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -108,7 +108,7 @@ PODS: - libwebp/mux (1.2.4): - libwebp/demux - libwebp/webp (1.2.4) - - local_auth (0.0.1): + - local_auth_ios (0.0.1): - Flutter - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) @@ -195,7 +195,7 @@ DEPENDENCIES: - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`) - - local_auth (from `.symlinks/plugins/local_auth/ios`) + - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) - motionphoto (from `.symlinks/plugins/motionphoto/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) @@ -276,8 +276,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_editor_common/ios" in_app_purchase_storekit: :path: ".symlinks/plugins/in_app_purchase_storekit/ios" - local_auth: - :path: ".symlinks/plugins/local_auth/ios" + local_auth_ios: + :path: ".symlinks/plugins/local_auth_ios/ios" media_extension: :path: ".symlinks/plugins/media_extension/ios" motionphoto: @@ -346,7 +346,7 @@ SPEC CHECKSUMS: image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 in_app_purchase_storekit: 6b297e2b5eab9fa3251a492d57301722e4132a71 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef - local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c + local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d media_extension: 6d30dc1431ebaa63f43c397c37917b1a0a597a4c motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 529def5d4..9cb678dfa 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -290,7 +290,7 @@ "${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework", "${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", - "${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework", + "${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework", "${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework", "${BUILT_PRODUCTS_DIR}/motionphoto/motionphoto.framework", "${BUILT_PRODUCTS_DIR}/move_to_background/move_to_background.framework", @@ -346,7 +346,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motionphoto.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/move_to_background.framework", diff --git a/lib/app.dart b/lib/app.dart index 20095996c..80f29ff0e 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -18,10 +18,12 @@ import "package:photos/utils/intent_util.dart"; class EnteApp extends StatefulWidget { final Future Function(String) runBackgroundTask; final Future Function(String) killBackgroundTask; + final AdaptiveThemeMode? savedThemeMode; const EnteApp( this.runBackgroundTask, - this.killBackgroundTask, { + this.killBackgroundTask, + this.savedThemeMode, { Key? key, }) : super(key: key); @@ -56,7 +58,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { return AdaptiveTheme( light: lightThemeData, dark: darkThemeData, - initial: AdaptiveThemeMode.system, + initial: widget.savedThemeMode ?? AdaptiveThemeMode.system, builder: (lightTheme, dartTheme) => MaterialApp( title: "ente", themeMode: ThemeMode.system, diff --git a/lib/events/sync_status_update_event.dart b/lib/events/sync_status_update_event.dart index 1fc62d8bd..f4fb712bb 100644 --- a/lib/events/sync_status_update_event.dart +++ b/lib/events/sync_status_update_event.dart @@ -1,6 +1,10 @@ +import "package:logging/logging.dart"; + import 'package:photos/events/event.dart'; class SyncStatusUpdate extends Event { + static final _logger = Logger("SyncStatusUpdate"); + final SyncStatus status; final int? completed; final int? total; @@ -18,6 +22,7 @@ class SyncStatusUpdate extends Event { this.reason = "", this.error, }) { + _logger.info("Creating sync status: " + status.toString()); timestamp = DateTime.now().microsecondsSinceEpoch; } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/lib/l10n/app_de.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 000000000..b8aaa42de --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,6 @@ +{ + "sign_up": "inscription", + "@sign_up": { + "description": "Text on the sign up button used during registration" + } +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/lib/l10n/app_it.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/lib/l10n/app_nl.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4304510c4..28c01dace 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import "package:adaptive_theme/adaptive_theme.dart"; import 'package:background_fetch/background_fetch.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; @@ -58,28 +59,38 @@ const kBackgroundLockLatency = Duration(seconds: 3); void main() async { debugRepaintRainbowEnabled = false; WidgetsFlutterBinding.ensureInitialized(); - await _runInForeground(); + final savedThemeMode = await AdaptiveTheme.getThemeMode(); + await _runInForeground(savedThemeMode); BackgroundFetch.registerHeadlessTask(_headlessTaskHandler); } -Future _runInForeground() async { +Future _runInForeground(AdaptiveThemeMode? savedThemeMode) async { return await _runWithLogs(() async { _logger.info("Starting app in foreground"); await _init(false, via: 'mainMethod'); unawaited(_scheduleFGSync('appStart in FG')); runApp( AppLock( - builder: (args) => const EnteApp(_runBackgroundTask, _killBGTask), + builder: (args) => + EnteApp(_runBackgroundTask, _killBGTask, savedThemeMode), lockScreen: const LockScreen(), enabled: Configuration.instance.shouldShowLockScreen(), lightTheme: lightThemeData, darkTheme: darkThemeData, backgroundLockLatency: kBackgroundLockLatency, + savedThemeMode: _themeMode(savedThemeMode), ), ); }); } +ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) { + if (savedThemeMode == null) return ThemeMode.system; + if (savedThemeMode.isLight) return ThemeMode.light; + if (savedThemeMode.isDark) return ThemeMode.dark; + return ThemeMode.system; +} + Future _runBackgroundTask(String taskId, {String mode = 'normal'}) async { if (_isProcessRunning) { _logger.info("Background task triggered when process was already running"); @@ -119,7 +130,7 @@ Future _runInBackground(String taskId) async { // https://stackoverflow.com/a/73796478/546896 @pragma('vm:entry-point') void _headlessTaskHandler(HeadlessTask task) { - print("_headlessTaskHandler"); + debugPrint("_headlessTaskHandler"); if (task.timeout) { BackgroundFetch.finish(task.taskId); } else { diff --git a/lib/models/api/storage_bonus/storage_bonus.dart b/lib/models/api/storage_bonus/storage_bonus.dart index 101b427ee..3a62beeb5 100644 --- a/lib/models/api/storage_bonus/storage_bonus.dart +++ b/lib/models/api/storage_bonus/storage_bonus.dart @@ -131,7 +131,8 @@ class BonusDetails { factory BonusDetails.fromJson(Map json) => BonusDetails( referralStats: List.from( - json["referralStats"].map((x) => ReferralStat.fromJson(x))), + json["referralStats"].map((x) => ReferralStat.fromJson(x)), + ), bonuses: List.from(json["bonuses"].map((x) => Bonus.fromJson(x))), refCount: json["refCount"], diff --git a/lib/models/device_collection.dart b/lib/models/device_collection.dart index 98991e069..337290bfb 100644 --- a/lib/models/device_collection.dart +++ b/lib/models/device_collection.dart @@ -11,6 +11,10 @@ class DeviceCollection { int? collectionID; File? thumbnail; + bool hasCollectionID() { + return collectionID != null && collectionID! != -1; + } + DeviceCollection( this.id, this.name, { diff --git a/lib/models/search/button_result.dart b/lib/models/search/button_result.dart index e4e2b4bdc..a9705f18d 100644 --- a/lib/models/search/button_result.dart +++ b/lib/models/search/button_result.dart @@ -1,4 +1,4 @@ -import "package:photos/ui/components/button_widget.dart"; +import 'package:photos/ui/components/buttons/button_widget.dart'; class ButtonResult { ///action can be null when action for the button that is returned when popping diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index 68c72b85e..03ba7158b 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -9,6 +9,7 @@ class UserDetails { final String email; final int usage; final int fileCount; + final int storageBonus; final int sharedCollectionsCount; final Subscription subscription; final FamilyData? familyData; @@ -17,6 +18,7 @@ class UserDetails { this.email, this.usage, this.fileCount, + this.storageBonus, this.sharedCollectionsCount, this.subscription, this.familyData, @@ -50,7 +52,8 @@ class UserDetails { } int getTotalStorage() { - return isPartOfFamily() ? familyData!.storage : subscription.storage; + return (isPartOfFamily() ? familyData!.storage : subscription.storage) + + storageBonus; } factory UserDetails.fromMap(Map map) { @@ -58,6 +61,7 @@ class UserDetails { map['email'] as String, map['usage'] as int, (map['fileCount'] ?? 0) as int, + (map['storageBonus'] ?? 0) as int, (map['sharedCollectionsCount'] ?? 0) as int, Subscription.fromMap(map['subscription']), FamilyData.fromMap(map['familyData']), @@ -69,6 +73,7 @@ class UserDetails { 'email': email, 'usage': usage, 'fileCount': fileCount, + 'storageBonus': storageBonus, 'sharedCollectionsCount': sharedCollectionsCount, 'subscription': subscription.toMap(), 'familyData': familyData?.toMap(), diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index 94be9643f..8d1942092 100644 --- a/lib/services/collections_service.dart +++ b/lib/services/collections_service.dart @@ -141,6 +141,7 @@ class CollectionsService { for (final collection in collections) { _cacheCollectionAttributes(collection); } + _logger.info("Collections synced"); watch.log("collection cache refresh"); if (fetchedCollections.isNotEmpty) { Bus.instance.fire( diff --git a/lib/services/favorites_service.dart b/lib/services/favorites_service.dart index 224cf0bfa..31ea94bcf 100644 --- a/lib/services/favorites_service.dart +++ b/lib/services/favorites_service.dart @@ -137,7 +137,10 @@ class FavoritesService { } Future updateFavorites( - BuildContext context, List files, bool favFlag) async { + BuildContext context, + List files, + bool favFlag, + ) async { final int currentUserID = Configuration.instance.getUserID()!; if (files.any((f) => f.uploadedFileID == null)) { throw AssertionError("Can only favorite uploaded items"); diff --git a/lib/services/feature_flag_service.dart b/lib/services/feature_flag_service.dart index 2d24dd08b..ad2d26791 100644 --- a/lib/services/feature_flag_service.dart +++ b/lib/services/feature_flag_service.dart @@ -14,6 +14,12 @@ class FeatureFlagService { static final FeatureFlagService instance = FeatureFlagService._privateConstructor(); static const _featureFlagsKey = "feature_flags_key"; + static final _internalUserIDs = const String.fromEnvironment( + "internal_user_ids", + defaultValue: "1,2,3,4,191", + ).split(",").map((element) { + return int.parse(element); + }).toSet(); final _logger = Logger("FeatureFlagService"); FeatureFlags? _featureFlags; @@ -64,7 +70,10 @@ class FeatureFlagService { bool isInternalUserOrDebugBuild() { final String? email = Configuration.instance.getEmail(); - return (email != null && email.endsWith("@ente.io")) || kDebugMode; + final userID = Configuration.instance.getUserID(); + return (email != null && email.endsWith("@ente.io")) || + _internalUserIDs.contains(userID) || + kDebugMode; } Future fetchFeatureFlags() async { diff --git a/lib/services/object_detection/object_detection_service.dart b/lib/services/object_detection/object_detection_service.dart index cead701f8..2a9706447 100644 --- a/lib/services/object_detection/object_detection_service.dart +++ b/lib/services/object_detection/object_detection_service.dart @@ -4,18 +4,20 @@ import "dart:typed_data"; import "package:logging/logging.dart"; import "package:photos/services/object_detection/models/predictions.dart"; import 'package:photos/services/object_detection/models/recognition.dart'; -import "package:photos/services/object_detection/tflite/classifier.dart"; +import 'package:photos/services/object_detection/tflite/cocossd_classifier.dart'; +import "package:photos/services/object_detection/tflite/mobilenet_classifier.dart"; +import "package:photos/services/object_detection/tflite/scene_classifier.dart"; import "package:photos/services/object_detection/utils/isolate_utils.dart"; class ObjectDetectionService { - static const scoreThreshold = 0.6; + static const scoreThreshold = 0.5; final _logger = Logger("ObjectDetectionService"); - /// Instance of [ObjectClassifier] - late ObjectClassifier _classifier; + late CocoSSDClassifier _objectClassifier; + late MobileNetClassifier _mobileNetClassifier; + late SceneClassifier _sceneClassifier; - /// Instance of [IsolateUtils] late IsolateUtils _isolateUtils; ObjectDetectionService._privateConstructor(); @@ -23,7 +25,9 @@ class ObjectDetectionService { Future init() async { _isolateUtils = IsolateUtils(); await _isolateUtils.start(); - _classifier = ObjectClassifier(); + _objectClassifier = CocoSSDClassifier(); + _mobileNetClassifier = MobileNetClassifier(); + _sceneClassifier = SceneClassifier(); } static ObjectDetectionService instance = @@ -31,18 +35,10 @@ class ObjectDetectionService { Future> predict(Uint8List bytes) async { try { - final isolateData = IsolateData( - bytes, - _classifier.interpreter.address, - _classifier.labels, - ); - final predictions = await _inference(isolateData); - final Set results = {}; - for (final Recognition result in predictions.recognitions) { - if (result.score > scoreThreshold) { - results.add(result.label); - } - } + final results = {}; + results.addAll(await _getObjects(bytes)); + results.addAll(await _getMobileNetResults(bytes)); + results.addAll(await _getSceneResults(bytes)); return results.toList(); } catch (e, s) { _logger.severe(e, s); @@ -50,6 +46,54 @@ class ObjectDetectionService { } } + Future> _getObjects(Uint8List bytes) async { + final isolateData = IsolateData( + bytes, + _objectClassifier.interpreter.address, + _objectClassifier.labels, + ClassifierType.cocossd, + ); + return _getPredictions(isolateData); + } + + Future> _getMobileNetResults(Uint8List bytes) async { + final isolateData = IsolateData( + bytes, + _mobileNetClassifier.interpreter.address, + _mobileNetClassifier.labels, + ClassifierType.mobilenet, + ); + return _getPredictions(isolateData); + } + + Future> _getSceneResults(Uint8List bytes) async { + final isolateData = IsolateData( + bytes, + _sceneClassifier.interpreter.address, + _sceneClassifier.labels, + ClassifierType.scenes, + ); + return _getPredictions(isolateData); + } + + Future> _getPredictions(IsolateData isolateData) async { + final predictions = await _inference(isolateData); + final Set results = {}; + for (final Recognition result in predictions.recognitions) { + if (result.score > scoreThreshold) { + results.add(result.label); + } + } + _logger.info( + "Time taken for " + + isolateData.type.toString() + + ": " + + predictions.stats.totalElapsedTime.toString() + + "ms", + ); + return results.toList(); + } + /// Runs inference in another isolate Future _inference(IsolateData isolateData) async { final responsePort = ReceivePort(); diff --git a/lib/services/object_detection/tflite/classifier.dart b/lib/services/object_detection/tflite/classifier.dart index 299b31244..73d1c561f 100644 --- a/lib/services/object_detection/tflite/classifier.dart +++ b/lib/services/object_detection/tflite/classifier.dart @@ -1,16 +1,25 @@ -import 'dart:math'; +import "dart:math"; -import 'package:image/image.dart' as imageLib; +import 'package:image/image.dart' as image_lib; import "package:logging/logging.dart"; -import 'package:photos/services/object_detection/models/predictions.dart'; -import 'package:photos/services/object_detection/models/recognition.dart'; -import "package:photos/services/object_detection/models/stats.dart"; +import "package:photos/services/object_detection/models/predictions.dart"; import "package:tflite_flutter/tflite_flutter.dart"; import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; -/// Classifier -class ObjectClassifier { - final _logger = Logger("Classifier"); +abstract class Classifier { + // Path to the model + String get modelPath; + + // Path to the labels + String get labelPath; + + // Input size expected by the model (for eg. width = height = 224) + int get inputSize; + + // Logger implementation for the specific classifier + Logger get logger; + + Predictions? predict(image_lib.Image image); /// Instance of Interpreter late Interpreter _interpreter; @@ -18,44 +27,30 @@ class ObjectClassifier { /// Labels file loaded as list late List _labels; - /// Input size of image (height = width = 300) - static const int inputSize = 300; - - /// Result score threshold - static const double threshold = 0.5; - - static const String modelFileName = "detect.tflite"; - static const String labelFileName = "labelmap.txt"; - - /// [ImageProcessor] used to pre-process the image - ImageProcessor? imageProcessor; - - /// Padding the image to transform into square - late int padSize; - /// Shapes of output tensors late List> _outputShapes; /// Types of output tensors late List _outputTypes; - /// Number of results to show - static const int numResults = 10; + /// Gets the interpreter instance + Interpreter get interpreter => _interpreter; - ObjectClassifier({ - Interpreter? interpreter, - List? labels, - }) { - loadModel(interpreter); - loadLabels(labels); - } + /// Gets the loaded labels + List get labels => _labels; + + /// Gets the output shapes + List> get outputShapes => _outputShapes; + + /// Gets the output types + List get outputTypes => _outputTypes; /// Loads interpreter from asset void loadModel(Interpreter? interpreter) async { try { _interpreter = interpreter ?? await Interpreter.fromAsset( - "models/" + modelFileName, + modelPath, options: InterpreterOptions()..threads = 4, ); final outputTensors = _interpreter.getOutputTensors(); @@ -65,115 +60,30 @@ class ObjectClassifier { _outputShapes.add(tensor.shape); _outputTypes.add(tensor.type); }); - _logger.info("Interpreter initialized"); + logger.info("Interpreter initialized"); } catch (e, s) { - _logger.severe("Error while creating interpreter", e, s); + logger.severe("Error while creating interpreter", e, s); } } /// Loads labels from assets void loadLabels(List? labels) async { try { - _labels = - labels ?? await FileUtil.loadLabels("assets/models/" + labelFileName); - _logger.info("Labels initialized"); + _labels = labels ?? await FileUtil.loadLabels(labelPath); + logger.info("Labels initialized"); } catch (e, s) { - _logger.severe("Error while loading labels", e, s); + logger.severe("Error while loading labels", e, s); } } /// Pre-process the image - TensorImage _getProcessedImage(TensorImage inputImage) { - padSize = max(inputImage.height, inputImage.width); - imageProcessor ??= ImageProcessorBuilder() + TensorImage getProcessedImage(TensorImage inputImage) { + final padSize = max(inputImage.height, inputImage.width); + final imageProcessor = ImageProcessorBuilder() .add(ResizeWithCropOrPadOp(padSize, padSize)) .add(ResizeOp(inputSize, inputSize, ResizeMethod.BILINEAR)) .build(); - inputImage = imageProcessor!.process(inputImage); + inputImage = imageProcessor.process(inputImage); return inputImage; } - - /// Runs object detection on the input image - Predictions? predict(imageLib.Image image) { - final predictStartTime = DateTime.now().millisecondsSinceEpoch; - - final preProcessStart = DateTime.now().millisecondsSinceEpoch; - - // Create TensorImage from image - TensorImage inputImage = TensorImage.fromImage(image); - - // Pre-process TensorImage - inputImage = _getProcessedImage(inputImage); - - final preProcessElapsedTime = - DateTime.now().millisecondsSinceEpoch - preProcessStart; - - // TensorBuffers for output tensors - final outputLocations = TensorBufferFloat(_outputShapes[0]); - final outputClasses = TensorBufferFloat(_outputShapes[1]); - final outputScores = TensorBufferFloat(_outputShapes[2]); - final numLocations = TensorBufferFloat(_outputShapes[3]); - - // Inputs object for runForMultipleInputs - // Use [TensorImage.buffer] or [TensorBuffer.buffer] to pass by reference - final inputs = [inputImage.buffer]; - - // Outputs map - final outputs = { - 0: outputLocations.buffer, - 1: outputClasses.buffer, - 2: outputScores.buffer, - 3: numLocations.buffer, - }; - - final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; - - // run inference - _interpreter.runForMultipleInputs(inputs, outputs); - - final inferenceTimeElapsed = - DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; - - // Maximum number of results to show - final resultsCount = min(numResults, numLocations.getIntValue(0)); - - // Using labelOffset = 1 as ??? at index 0 - const labelOffset = 1; - - final recognitions = []; - - for (int i = 0; i < resultsCount; i++) { - // Prediction score - final score = outputScores.getDoubleValue(i); - - // Label string - final labelIndex = outputClasses.getIntValue(i) + labelOffset; - final label = _labels.elementAt(labelIndex); - - if (score > threshold) { - recognitions.add( - Recognition(i, label, score), - ); - } - } - - final predictElapsedTime = - DateTime.now().millisecondsSinceEpoch - predictStartTime; - _logger.info(recognitions); - return Predictions( - recognitions, - Stats( - predictElapsedTime, - predictElapsedTime, - inferenceTimeElapsed, - preProcessElapsedTime, - ), - ); - } - - /// Gets the interpreter instance - Interpreter get interpreter => _interpreter; - - /// Gets the loaded labels - List get labels => _labels; } diff --git a/lib/services/object_detection/tflite/cocossd_classifier.dart b/lib/services/object_detection/tflite/cocossd_classifier.dart new file mode 100644 index 000000000..4722c0e86 --- /dev/null +++ b/lib/services/object_detection/tflite/cocossd_classifier.dart @@ -0,0 +1,115 @@ +import 'dart:math'; + +import 'package:image/image.dart' as image_lib; +import "package:logging/logging.dart"; +import 'package:photos/services/object_detection/models/predictions.dart'; +import 'package:photos/services/object_detection/models/recognition.dart'; +import "package:photos/services/object_detection/models/stats.dart"; +import "package:photos/services/object_detection/tflite/classifier.dart"; +import "package:tflite_flutter/tflite_flutter.dart"; +import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; + +/// Classifier +class CocoSSDClassifier extends Classifier { + static final _logger = Logger("CocoSSDClassifier"); + static const double threshold = 0.5; + + @override + String get modelPath => "models/cocossd/model.tflite"; + + @override + String get labelPath => "assets/models/cocossd/labels.txt"; + + @override + int get inputSize => 300; + + @override + Logger get logger => _logger; + + static const int numResults = 10; + + CocoSSDClassifier({ + Interpreter? interpreter, + List? labels, + }) { + loadModel(interpreter); + loadLabels(labels); + } + + @override + Predictions? predict(image_lib.Image image) { + final predictStartTime = DateTime.now().millisecondsSinceEpoch; + + final preProcessStart = DateTime.now().millisecondsSinceEpoch; + + // Create TensorImage from image + TensorImage inputImage = TensorImage.fromImage(image); + + // Pre-process TensorImage + inputImage = getProcessedImage(inputImage); + + final preProcessElapsedTime = + DateTime.now().millisecondsSinceEpoch - preProcessStart; + + // TensorBuffers for output tensors + final outputLocations = TensorBufferFloat(outputShapes[0]); + final outputClasses = TensorBufferFloat(outputShapes[1]); + final outputScores = TensorBufferFloat(outputShapes[2]); + final numLocations = TensorBufferFloat(outputShapes[3]); + + // Inputs object for runForMultipleInputs + // Use [TensorImage.buffer] or [TensorBuffer.buffer] to pass by reference + final inputs = [inputImage.buffer]; + + // Outputs map + final outputs = { + 0: outputLocations.buffer, + 1: outputClasses.buffer, + 2: outputScores.buffer, + 3: numLocations.buffer, + }; + + final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; + + // run inference + interpreter.runForMultipleInputs(inputs, outputs); + + final inferenceTimeElapsed = + DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; + + // Maximum number of results to show + final resultsCount = min(numResults, numLocations.getIntValue(0)); + + // Using labelOffset = 1 as ??? at index 0 + const labelOffset = 1; + + final recognitions = []; + + for (int i = 0; i < resultsCount; i++) { + // Prediction score + final score = outputScores.getDoubleValue(i); + + // Label string + final labelIndex = outputClasses.getIntValue(i) + labelOffset; + final label = labels.elementAt(labelIndex); + + if (score > threshold) { + recognitions.add( + Recognition(i, label, score), + ); + } + } + + final predictElapsedTime = + DateTime.now().millisecondsSinceEpoch - predictStartTime; + return Predictions( + recognitions, + Stats( + predictElapsedTime, + predictElapsedTime, + inferenceTimeElapsed, + preProcessElapsedTime, + ), + ); + } +} diff --git a/lib/services/object_detection/tflite/mobilenet_classifier.dart b/lib/services/object_detection/tflite/mobilenet_classifier.dart new file mode 100644 index 000000000..35f35c91b --- /dev/null +++ b/lib/services/object_detection/tflite/mobilenet_classifier.dart @@ -0,0 +1,84 @@ +import 'package:image/image.dart' as image_lib; +import "package:logging/logging.dart"; +import 'package:photos/services/object_detection/models/predictions.dart'; +import 'package:photos/services/object_detection/models/recognition.dart'; +import "package:photos/services/object_detection/models/stats.dart"; +import "package:photos/services/object_detection/tflite/classifier.dart"; +import "package:tflite_flutter/tflite_flutter.dart"; +import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; + +// Source: https://tfhub.dev/tensorflow/lite-model/mobilenet_v1_1.0_224/1/default/1 +class MobileNetClassifier extends Classifier { + static final _logger = Logger("MobileNetClassifier"); + static const double threshold = 0.5; + + @override + String get modelPath => "models/mobilenet/mobilenet_v1_1.0_224_quant.tflite"; + + @override + String get labelPath => + "assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt"; + + @override + int get inputSize => 224; + + @override + Logger get logger => _logger; + + MobileNetClassifier({ + Interpreter? interpreter, + List? labels, + }) { + loadModel(interpreter); + loadLabels(labels); + } + + @override + Predictions? predict(image_lib.Image image) { + final predictStartTime = DateTime.now().millisecondsSinceEpoch; + + final preProcessStart = DateTime.now().millisecondsSinceEpoch; + + // Create TensorImage from image + TensorImage inputImage = TensorImage.fromImage(image); + + // Pre-process TensorImage + inputImage = getProcessedImage(inputImage); + + final preProcessElapsedTime = + DateTime.now().millisecondsSinceEpoch - preProcessStart; + + // TensorBuffers for output tensors + final output = TensorBufferUint8(outputShapes[0]); + final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; + // run inference + interpreter.run(inputImage.buffer, output.buffer); + + final inferenceTimeElapsed = + DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; + + final recognitions = []; + for (int i = 0; i < labels.length; i++) { + final score = output.getDoubleValue(i) / 255; + if (score >= threshold) { + final label = labels.elementAt(i); + + recognitions.add( + Recognition(i, label, score), + ); + } + } + + final predictElapsedTime = + DateTime.now().millisecondsSinceEpoch - predictStartTime; + return Predictions( + recognitions, + Stats( + predictElapsedTime, + predictElapsedTime, + inferenceTimeElapsed, + preProcessElapsedTime, + ), + ); + } +} diff --git a/lib/services/object_detection/tflite/scene_classifier.dart b/lib/services/object_detection/tflite/scene_classifier.dart new file mode 100644 index 000000000..b1b8cd16e --- /dev/null +++ b/lib/services/object_detection/tflite/scene_classifier.dart @@ -0,0 +1,82 @@ +import 'package:image/image.dart' as image_lib; +import "package:logging/logging.dart"; +import 'package:photos/services/object_detection/models/predictions.dart'; +import 'package:photos/services/object_detection/models/recognition.dart'; +import "package:photos/services/object_detection/models/stats.dart"; +import "package:photos/services/object_detection/tflite/classifier.dart"; +import "package:tflite_flutter/tflite_flutter.dart"; +import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; + +// Source: https://tfhub.dev/sayannath/lite-model/image-scene/1 +class SceneClassifier extends Classifier { + static final _logger = Logger("SceneClassifier"); + static const double threshold = 0.5; + + @override + String get modelPath => "models/scenes/model.tflite"; + + @override + String get labelPath => "assets/models/scenes/labels.txt"; + + @override + int get inputSize => 224; + + @override + Logger get logger => _logger; + + SceneClassifier({ + Interpreter? interpreter, + List? labels, + }) { + loadModel(interpreter); + loadLabels(labels); + } + + @override + Predictions? predict(image_lib.Image image) { + final predictStartTime = DateTime.now().millisecondsSinceEpoch; + + final preProcessStart = DateTime.now().millisecondsSinceEpoch; + + // Create TensorImage from image + TensorImage inputImage = TensorImage.fromImage(image); + + // Pre-process TensorImage + inputImage = getProcessedImage(inputImage); + final list = inputImage.getTensorBuffer().getDoubleList(); + final input = list.reshape([1, inputSize, inputSize, 3]); + + final preProcessElapsedTime = + DateTime.now().millisecondsSinceEpoch - preProcessStart; + + final output = TensorBufferFloat(outputShapes[0]); + + final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; + interpreter.run(input, output.buffer); + final inferenceTimeElapsed = + DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; + + final recognitions = []; + for (int i = 0; i < labels.length; i++) { + final score = output.getDoubleValue(i); + final label = labels.elementAt(i); + if (score >= threshold) { + recognitions.add( + Recognition(i, label, score), + ); + } + } + + final predictElapsedTime = + DateTime.now().millisecondsSinceEpoch - predictStartTime; + return Predictions( + recognitions, + Stats( + predictElapsedTime, + predictElapsedTime, + inferenceTimeElapsed, + preProcessElapsedTime, + ), + ); + } +} diff --git a/lib/services/object_detection/utils/isolate_utils.dart b/lib/services/object_detection/utils/isolate_utils.dart index 2d55424d4..e1c85e4b2 100644 --- a/lib/services/object_detection/utils/isolate_utils.dart +++ b/lib/services/object_detection/utils/isolate_utils.dart @@ -3,6 +3,9 @@ import "dart:typed_data"; import 'package:image/image.dart' as imgLib; import "package:photos/services/object_detection/tflite/classifier.dart"; +import 'package:photos/services/object_detection/tflite/cocossd_classifier.dart'; +import "package:photos/services/object_detection/tflite/mobilenet_classifier.dart"; +import "package:photos/services/object_detection/tflite/scene_classifier.dart"; import 'package:tflite_flutter/tflite_flutter.dart'; /// Manages separate Isolate instance for inference @@ -29,15 +32,32 @@ class IsolateUtils { sendPort.send(port.sendPort); await for (final IsolateData isolateData in port) { - final classifier = ObjectClassifier( - interpreter: Interpreter.fromAddress(isolateData.interpreterAddress), - labels: isolateData.labels, - ); + final classifier = _getClassifier(isolateData); final image = imgLib.decodeImage(isolateData.input); final results = classifier.predict(image!); isolateData.responsePort.send(results); } } + + static Classifier _getClassifier(IsolateData isolateData) { + final interpreter = Interpreter.fromAddress(isolateData.interpreterAddress); + if (isolateData.type == ClassifierType.cocossd) { + return CocoSSDClassifier( + interpreter: interpreter, + labels: isolateData.labels, + ); + } else if (isolateData.type == ClassifierType.mobilenet) { + return MobileNetClassifier( + interpreter: interpreter, + labels: isolateData.labels, + ); + } else { + return SceneClassifier( + interpreter: interpreter, + labels: isolateData.labels, + ); + } + } } /// Bundles data to pass between Isolate @@ -45,11 +65,19 @@ class IsolateData { Uint8List input; int interpreterAddress; List labels; + ClassifierType type; late SendPort responsePort; IsolateData( this.input, this.interpreterAddress, this.labels, + this.type, ); } + +enum ClassifierType { + cocossd, + mobilenet, + scenes, +} diff --git a/lib/services/remote_sync_service.dart b/lib/services/remote_sync_service.dart index a1a137eac..e80a15b4e 100644 --- a/lib/services/remote_sync_service.dart +++ b/lib/services/remote_sync_service.dart @@ -23,6 +23,7 @@ import 'package:photos/models/file_type.dart'; import 'package:photos/models/upload_strategy.dart'; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; +import "package:photos/services/feature_flag_service.dart"; import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/local_file_update_service.dart'; import 'package:photos/services/sync_service.dart'; @@ -44,7 +45,7 @@ class RemoteSyncService { int _completedUploads = 0; late SharedPreferences _prefs; Completer? _existingSync; - bool _existingSyncSilent = false; + bool _isExistingSyncSilent = false; static const kHasSyncedArchiveKey = "has_synced_archive"; final String _isFirstRemoteSyncDone = "isFirstRemoteSyncDone"; @@ -84,13 +85,17 @@ class RemoteSyncService { _logger.info("Remote sync already in progress, skipping"); // if current sync is silent but request sync is non-silent (demands UI // updates), update the syncSilently flag - if (_existingSyncSilent == true && silently == false) { - _existingSyncSilent = false; + if (_isExistingSyncSilent && !silently) { + _isExistingSyncSilent = false; } return _existingSync?.future; } _existingSync = Completer(); - _existingSyncSilent = silently; + _isExistingSyncSilent = silently; + _logger.info( + "Starting remote sync " + + (silently ? "silently" : " with status updates"), + ); try { // use flag to decide if we should start marking files for upload before @@ -115,18 +120,20 @@ class RemoteSyncService { } final filesToBeUploaded = await _getFilesToBeUploaded(); final hasUploadedFiles = await _uploadFiles(filesToBeUploaded); + _logger.info("File upload complete"); if (hasUploadedFiles) { await _pullDiff(); _existingSync?.complete(); _existingSync = null; await syncDeviceCollectionFilesForUpload(); final hasMoreFilesToBackup = (await _getFilesToBeUploaded()).isNotEmpty; + _logger.info("hasMoreFilesToBackup?" + hasMoreFilesToBackup.toString()); if (hasMoreFilesToBackup && !_shouldThrottleSync()) { // Skipping a resync to ensure that files that were ignored in this // session are not processed now sync(); } else { - debugPrint("Fire backup completed event"); + _logger.info("Fire backup completed event"); Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup)); } } else { @@ -146,13 +153,17 @@ class RemoteSyncService { rethrow; } else { _logger.severe("Error executing remote sync ", e, s); + if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) { + rethrow; + } } } finally { - _existingSyncSilent = false; + _isExistingSyncSilent = false; } } Future _pullDiff() async { + _logger.info("Pulling remote diff"); final isFirstSync = !_collectionsService.hasSyncedCollections(); await _collectionsService.sync(); // check and reset user's collection syncTime in past for older clients @@ -179,6 +190,7 @@ class RemoteSyncService { ); await _collectionsService.setCollectionSyncTime(c.id, c.updationTime); } + _logger.info("All updated collections synced"); } Future _resetAllCollectionsSyncTime() async { @@ -193,7 +205,12 @@ class RemoteSyncService { } Future _syncCollectionDiff(int collectionID, int sinceTime) async { - if (!_existingSyncSilent) { + _logger.info( + "Syncing collection #" + + collectionID.toString() + + (_isExistingSyncSilent ? " silently" : ""), + ); + if (!_isExistingSyncSilent) { Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff)); } final diff = @@ -252,10 +269,13 @@ class RemoteSyncService { collectionID, _collectionsService.getCollectionSyncTime(collectionID), ); + } else { + _logger.info("Collection #" + collectionID.toString() + " synced"); } } Future syncDeviceCollectionFilesForUpload() async { + _logger.info("Syncing device collections to be uploaded"); final int ownerID = _config.getUserID()!; final deviceCollections = await _db.getDeviceCollections(); @@ -279,15 +299,15 @@ class RemoteSyncService { if (localIDsToSync.isEmpty) { continue; } - await _createCollectionForDevicePath(deviceCollection); - if (deviceCollection.collectionID == -1) { - _logger.finest('DeviceCollection should not be -1 here'); + final collectionID = await _getCollectionID(deviceCollection); + if (collectionID == null) { + _logger.warning('DeviceCollection was either deleted or missing'); continue; } moreFilesMarkedForBackup = true; await _db.setCollectionIDForUnMappedLocalFiles( - deviceCollection.collectionID!, + collectionID, localIDsToSync, ); @@ -295,15 +315,13 @@ class RemoteSyncService { // the collection. This can happen when a user has marked a folder // for sync, then un-synced it and again tries to mark if for sync. final Set existingMapping = - await _db.getLocalFileIDsForCollection( - deviceCollection.collectionID!, - ); + await _db.getLocalFileIDsForCollection(collectionID); final Set commonElements = localIDsToSync.intersection(existingMapping); if (commonElements.isNotEmpty) { debugPrint( "${commonElements.length} files already existing in " - "collection ${deviceCollection.collectionID} for ${deviceCollection.name}", + "collection $collectionID for ${deviceCollection.name}", ); localIDsToSync.removeAll(commonElements); } @@ -324,7 +342,7 @@ class RemoteSyncService { final String localID = existingFile.localID!; if (!fileFoundForLocalIDs.contains(localID)) { existingFile.generatedID = null; - existingFile.collectionID = deviceCollection.collectionID; + existingFile.collectionID = collectionID; existingFile.uploadedFileID = null; existingFile.ownerID = null; newFilesToInsert.add(existingFile); @@ -414,27 +432,37 @@ class RemoteSyncService { } } - Future _createCollectionForDevicePath( - DeviceCollection deviceCollection, - ) async { - int deviceCollectionID = deviceCollection.collectionID ?? -1; - if (deviceCollectionID != -1) { - final collectionByID = - _collectionsService.getCollectionByID(deviceCollectionID); - if (collectionByID == null || collectionByID.isDeleted) { - _logger.info( - "Collection $deviceCollectionID either deleted or missing " - "for path ${deviceCollection.id}", + Future _getCollectionID(DeviceCollection deviceCollection) async { + if (deviceCollection.hasCollectionID()) { + final collection = + _collectionsService.getCollectionByID(deviceCollection.collectionID!); + if (collection != null && !collection.isDeleted) { + return collection.id; + } + if (collection == null) { + // ideally, this should never happen because the app keeps a track of + // all collections and their IDs. But, if somehow the collection is + // deleted, we should fetch it again + _logger.severe( + "Collection ${deviceCollection.collectionID} missing " + "for pathID ${deviceCollection.id}", ); - deviceCollectionID = -1; + _collectionsService + .fetchCollectionByID(deviceCollection.collectionID!) + .ignore(); + // return, by next run collection should be available. + // we are not waiting on fetch by choice because device might have wrong + // mapping which will result in breaking upload for other device path + return null; + } else if (collection.isDeleted) { + _logger.warning("Collection ${deviceCollection.collectionID} deleted " + "for pathID ${deviceCollection.id}, new collection will be created"); } } - if (deviceCollectionID == -1) { - final collection = - await _collectionsService.getOrCreateForPath(deviceCollection.name); - await _db.updateDeviceCollection(deviceCollection.id, collection.id); - deviceCollection.collectionID = collection.id; - } + final collection = + await _collectionsService.getOrCreateForPath(deviceCollection.name); + await _db.updateDeviceCollection(deviceCollection.id, collection.id); + return collection.id; } Future> _getFilesToBeUploaded() async { diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index c9209e91f..c2435e12a 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -2,7 +2,6 @@ import "dart:convert"; import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network/network.dart'; import 'package:photos/data/holidays.dart'; import 'package:photos/data/months.dart'; import 'package:photos/data/years.dart'; @@ -24,7 +23,6 @@ import 'package:tuple/tuple.dart'; class SearchService { Future>? _cachedFilesFuture; - final _enteDio = NetworkClient.instance.enteDio; final _logger = Logger((SearchService).toString()); final _collectionService = CollectionsService.instance; static const _maximumResultsLimit = 20; diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 2b619baeb..7eaa3d65b 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:bip39/bip39.dart' as bip39; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; +import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/core/network/network.dart'; import 'package:photos/db/public_keys_db.dart'; @@ -23,7 +25,9 @@ import 'package:photos/ui/account/ott_verification_page.dart'; import 'package:photos/ui/account/password_entry_page.dart'; import 'package:photos/ui/account/password_reentry_page.dart'; import 'package:photos/ui/account/two_factor_authentication_page.dart'; +import 'package:photos/ui/account/two_factor_recovery_page.dart'; import 'package:photos/ui/account/two_factor_setup_page.dart'; +import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; import 'package:photos/utils/toast_util.dart'; @@ -112,6 +116,17 @@ class UserService { } } + Future sendFeedback( + BuildContext context, + String feedback, { + String type = "SubCancellation", + }) async { + await _dio.post( + _config.getHttpEndpoint() + "/anonymous/feedback", + data: {"feedback": feedback, "type": "type"}, + ); + } + // getPublicKey returns null value if email id is not // associated with another ente account Future getPublicKey(String email) async { @@ -153,6 +168,10 @@ class UserService { final userDetails = UserDetails.fromMap(response.data); if (shouldCache) { await _preferences.setString(keyUserDetails, userDetails.toJson()); + // handle email change from different client + if (userDetails.email != _config.getEmail()) { + setEmail(userDetails.email); + } } return userDetails; } on DioError catch (e) { @@ -218,13 +237,9 @@ class UserService { Future getDeleteChallenge( BuildContext context, ) async { - final dialog = createProgressDialog(context, "Please wait..."); - await dialog.show(); try { final response = await _enteDio.get("/users/delete-challenge"); if (response.statusCode == 200) { - // clear data - await dialog.hide(); return DeleteChallengeResponse( allowDelete: response.data["allowDelete"] as bool, encryptedChallenge: response.data["encryptedChallenge"], @@ -234,7 +249,6 @@ class UserService { } } catch (e) { _logger.severe(e); - await dialog.hide(); await showGenericErrorDialog(context: context); return null; } @@ -242,13 +256,17 @@ class UserService { Future deleteAccount( BuildContext context, - String challengeResponse, - ) async { + String challengeResponse, { + required String reasonCategory, + required String feedback, + }) async { try { final response = await _enteDio.delete( "/users/delete", data: { "challenge": challengeResponse, + "reasonCategory": reasonCategory, + "feedback": feedback, }, ); if (response.statusCode == 200) { @@ -489,6 +507,147 @@ class UserService { } } + Future recoverTwoFactor(BuildContext context, String sessionID) async { + final dialog = createProgressDialog(context, "Please wait..."); + await dialog.show(); + try { + final response = await _dio.get( + _config.getHttpEndpoint() + "/users/two-factor/recover", + queryParameters: { + "sessionID": sessionID, + }, + ); + if (response.statusCode == 200) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) { + return TwoFactorRecoveryPage( + sessionID, + response.data["encryptedSecret"], + response.data["secretDecryptionNonce"], + ); + }, + ), + (route) => route.isFirst, + ); + } + } on DioError catch (e) { + _logger.severe(e); + if (e.response != null && e.response!.statusCode == 404) { + showToast(context, "Session expired"); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) { + return const LoginPage(); + }, + ), + (route) => route.isFirst, + ); + } else { + showErrorDialog( + context, + "Oops", + "Something went wrong, please try again", + ); + } + } catch (e) { + _logger.severe(e); + showErrorDialog( + context, + "Oops", + "Something went wrong, please try again", + ); + } finally { + await dialog.hide(); + } + } + + Future removeTwoFactor( + BuildContext context, + String sessionID, + String recoveryKey, + String encryptedSecret, + String secretDecryptionNonce, + ) async { + final dialog = createProgressDialog(context, "Please wait..."); + await dialog.show(); + String secret; + try { + if (recoveryKey.contains(' ')) { + if (recoveryKey.split(' ').length != mnemonicKeyWordCount) { + throw AssertionError( + 'recovery code should have $mnemonicKeyWordCount words', + ); + } + recoveryKey = bip39.mnemonicToEntropy(recoveryKey); + } + secret = CryptoUtil.bin2base64( + await CryptoUtil.decrypt( + CryptoUtil.base642bin(encryptedSecret), + CryptoUtil.hex2bin(recoveryKey.trim()), + CryptoUtil.base642bin(secretDecryptionNonce), + ), + ); + } catch (e) { + await dialog.hide(); + await showErrorDialog( + context, + "Incorrect recovery key", + "The recovery key you entered is incorrect", + ); + return; + } + try { + final response = await _dio.post( + _config.getHttpEndpoint() + "/users/two-factor/remove", + data: { + "sessionID": sessionID, + "secret": secret, + }, + ); + if (response.statusCode == 200) { + showShortToast(context, "Two-factor authentication successfully reset"); + await _saveConfiguration(response); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) { + return const PasswordReentryPage(); + }, + ), + (route) => route.isFirst, + ); + } + } on DioError catch (e) { + _logger.severe(e); + if (e.response != null && e.response!.statusCode == 404) { + showToast(context, "Session expired"); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) { + return const LoginPage(); + }, + ), + (route) => route.isFirst, + ); + } else { + showErrorDialog( + context, + "Oops", + "Something went wrong, please try again", + ); + } + } catch (e) { + _logger.severe(e); + showErrorDialog( + context, + "Oops", + "Something went wrong, please try again", + ); + } finally { + await dialog.hide(); + } + } + Future setupTwoFactor(BuildContext context, Completer completer) async { final dialog = createProgressDialog(context, "Please wait..."); await dialog.show(); @@ -518,13 +677,26 @@ class UserService { String secret, String code, ) async { + Uint8List recoveryKey; + try { + recoveryKey = await getOrCreateRecoveryKey(context); + } catch (e) { + showGenericErrorDialog(context: context); + return false; + } final dialog = createProgressDialog(context, "Verifying..."); await dialog.show(); + final encryptionResult = + CryptoUtil.encryptSync(CryptoUtil.base642bin(secret), recoveryKey); try { await _enteDio.post( "/users/two-factor/enable", data: { - "code": code + "code": code, + "encryptedTwoFactorSecret": + CryptoUtil.bin2base64(encryptionResult.encryptedData!), + "twoFactorSecretDecryptionNonce": + CryptoUtil.bin2base64(encryptionResult.nonce!), }, ); await dialog.hide(); diff --git a/lib/ui/account/delete_account_page.dart b/lib/ui/account/delete_account_page.dart index c629469a6..d4b3df516 100644 --- a/lib/ui/account/delete_account_page.dart +++ b/lib/ui/account/delete_account_page.dart @@ -1,22 +1,43 @@ import 'dart:convert'; +import "package:dropdown_button2/dropdown_button2.dart"; import 'package:flutter/material.dart'; +import "package:logging/logging.dart"; import 'package:photos/core/configuration.dart'; import 'package:photos/models/delete_account.dart'; -import 'package:photos/services/local_authentication_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/email_util.dart'; +import "package:photos/utils/toast_util.dart"; -class DeleteAccountPage extends StatelessWidget { +class DeleteAccountPage extends StatefulWidget { const DeleteAccountPage({ Key? key, }) : super(key: key); + @override + State createState() => _DeleteAccountPageState(); +} + +class _DeleteAccountPageState extends State { + bool _hasConfirmedDeletion = false; + final _feedbackTextCtrl = TextEditingController(); + final String _defaultSelection = 'Select reason'; + late String _dropdownValue = _defaultSelection; + late final List _deletionReason = [ + _defaultSelection, + 'It’s missing a key feature that I need', + 'The app or a certain feature does not \nbehave as I think it should', + 'I found another service that I like better', + 'I use a different account', + 'My reason isn’t listed', + ]; + final List _reasonIndexesWhereFeedbackIsNecessary = [1, 2, 5]; + @override Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); @@ -37,68 +58,150 @@ class DeleteAccountPage extends StatelessWidget { child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, children: [ - Image.asset( - 'assets/broken_heart.png', - width: 200, + Text( + "What is the main reason you are deleting your account?", + style: getEnteTextTheme(context).body, ), - const SizedBox( - height: 24, - ), - Center( - child: Text( - "We'll be sorry to see you go. Are you facing some issue?", - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith(color: colorScheme.textMuted), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: 48, + decoration: BoxDecoration( + color: colorScheme.fillFaint, + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton2( + alignment: AlignmentDirectional.topStart, + value: _dropdownValue, + onChanged: (String? newValue) { + setState(() { + _dropdownValue = newValue!; + }); + }, + underline: const SizedBox(), + items: _deletionReason + .map>((String value) { + return DropdownMenuItem( + value: value, + enabled: value != _defaultSelection, + alignment: Alignment.centerLeft, + child: Text( + value, + style: value != _defaultSelection + ? getEnteTextTheme(context).small + : getEnteTextTheme(context).smallMuted, + ), + ); + }).toList(), ), ), - const SizedBox( - height: 12, + const SizedBox(height: 24), + Text( + "We are sorry to see you go. Please share your feedback to " + "help us improve.", + style: getEnteTextTheme(context).body, ), - RichText( - // textAlign: TextAlign.center, - text: TextSpan( - children: const [ - TextSpan(text: "Please write to us at "), - TextSpan( - text: "feedback@ente.io", - style: TextStyle(color: Color.fromRGBO(29, 185, 84, 1)), - ), - TextSpan( - text: ", maybe there is a way we can help.", - ), - ], - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith(color: colorScheme.textMuted), + const SizedBox(height: 4), + TextFormField( + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: + BorderSide(color: colorScheme.strokeFaint, width: 1), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: + BorderSide(color: colorScheme.strokeFaint, width: 1), + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.transparent, + hintText: "Feedback", + contentPadding: const EdgeInsets.all(12), ), - ), - const SizedBox( - height: 24, - ), - ButtonWidget( - buttonType: ButtonType.primary, - labelText: "Yes, send feedback", - icon: Icons.check_outlined, - onTap: () async { - await sendEmail( - context, - to: 'feedback@ente.io', - subject: '[Feedback]', - ); + controller: _feedbackTextCtrl, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.multiline, + minLines: 3, + maxLines: null, + onChanged: (_) { + setState(() {}); }, ), - const SizedBox(height: 8), - ButtonWidget( - buttonType: ButtonType.tertiaryCritical, - labelText: "No, delete account", - icon: Icons.no_accounts_outlined, - onTap: () async => {await _initiateDelete(context)}, - shouldSurfaceExecutionStates: false, - ) + _shouldAskForFeedback() + ? SizedBox( + height: 42, + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + "Kindly help us with this information", + style: getEnteTextTheme(context) + .smallBold + .copyWith(color: colorScheme.warning700), + ), + ), + ) + : const SizedBox(height: 42), + GestureDetector( + onTap: () { + setState(() { + _hasConfirmedDeletion = !_hasConfirmedDeletion; + }); + }, + child: Row( + children: [ + Checkbox( + value: _hasConfirmedDeletion, + side: CheckboxTheme.of(context).side, + onChanged: (value) { + setState(() { + _hasConfirmedDeletion = value!; + }); + }, + ), + Expanded( + child: Text( + "Yes, I want to permanently delete this account and " + "all its data.", + style: getEnteTextTheme(context).bodyMuted, + textAlign: TextAlign.left, + ), + ) + ], + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ButtonWidget( + buttonType: ButtonType.critical, + labelText: "Confirm Account Deletion", + isDisabled: _shouldBlockDeletion(), + onTap: () async { + await _initiateDelete(context); + }, + shouldSurfaceExecutionStates: true, + ), + const SizedBox(height: 8), + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: "Cancel", + onTap: () async { + Navigator.of(context).pop(); + }, + ), + const SafeArea( + child: SizedBox( + height: 12, + ), + ), + ], + ), + ), ], ), ), @@ -106,58 +209,69 @@ class DeleteAccountPage extends StatelessWidget { ); } + bool _shouldBlockDeletion() { + return !_hasConfirmedDeletion || + _dropdownValue == _defaultSelection || + _shouldAskForFeedback(); + } + + bool _shouldAskForFeedback() { + return (_reasonIndexesWhereFeedbackIsNecessary + .contains(_deletionReason.indexOf(_dropdownValue)) && + _feedbackTextCtrl.text.trim().isEmpty); + } + Future _initiateDelete(BuildContext context) async { - final deleteChallengeResponse = - await UserService.instance.getDeleteChallenge(context); - if (deleteChallengeResponse == null) { - return; - } - if (deleteChallengeResponse.allowDelete) { - await _confirmAndDelete(context, deleteChallengeResponse); - } else { - await _requestEmailForDeletion(context); + final choice = await showChoiceDialog( + context, + title: "Confirm Account Deletion", + body: "You are about to permanently delete your account and all its data." + "\nThis action is irreversible.", + firstButtonLabel: "Delete Account Permanently", + firstButtonType: ButtonType.critical, + firstButtonOnTap: () async { + final deleteChallengeResponse = + await UserService.instance.getDeleteChallenge(context); + if (deleteChallengeResponse == null) { + return; + } + if (deleteChallengeResponse.allowDelete) { + await _delete(context, deleteChallengeResponse); + } else { + await _requestEmailForDeletion(context); + } + }, + isDismissible: false, + ); + if (choice!.action == ButtonAction.error) { + await showGenericErrorDialog(context: context); } } - Future _confirmAndDelete( + Future _delete( BuildContext context, DeleteChallengeResponse response, ) async { - final hasAuthenticated = - await LocalAuthenticationService.instance.requestLocalAuthentication( - context, - "Please authenticate to initiate account deletion", - ); - - if (hasAuthenticated) { - final choice = await showChoiceDialog( - context, - title: 'Are you sure you want to delete your account?', - body: - 'Your uploaded data will be scheduled for deletion, and your account' - ' will be permanently deleted. \n\nThis action is not reversible.', - firstButtonLabel: "Delete my account", - isCritical: true, - firstButtonOnTap: () async { - final decryptChallenge = CryptoUtil.openSealSync( - CryptoUtil.base642bin(response.encryptedChallenge), - CryptoUtil.base642bin( - Configuration.instance.getKeyAttributes()!.publicKey, - ), - Configuration.instance.getSecretKey()!, - ); - final challengeResponseStr = utf8.decode(decryptChallenge); - await UserService.instance - .deleteAccount(context, challengeResponseStr); - }, + try { + final decryptChallenge = CryptoUtil.openSealSync( + CryptoUtil.base642bin(response.encryptedChallenge), + CryptoUtil.base642bin( + Configuration.instance.getKeyAttributes()!.publicKey, + ), + Configuration.instance.getSecretKey()!, + ); + final challengeResponseStr = utf8.decode(decryptChallenge); + await UserService.instance.deleteAccount( + context, + challengeResponseStr, + reasonCategory: _dropdownValue, + feedback: _feedbackTextCtrl.text.trim(), ); - if (choice!.action == ButtonAction.error) { - showGenericErrorDialog(context: context); - } - if (choice.action != ButtonAction.first) { - return; - } Navigator.of(context).popUntil((route) => route.isFirst); + showShortToast(context, "Your account has been deleted"); + } catch (e, s) { + Logger("DeleteAccount").severe("failed to delete", e, s); + showGenericErrorDialog(context: context); } } diff --git a/lib/ui/account/email_entry_page.dart b/lib/ui/account/email_entry_page.dart index 1c6efe845..62e9065fc 100644 --- a/lib/ui/account/email_entry_page.dart +++ b/lib/ui/account/email_entry_page.dart @@ -258,7 +258,9 @@ class _EmailEntryPageState extends State { autofillHints: const [AutofillHints.newPassword], onEditingComplete: () => TextInput.finishAutofillContext(), decoration: InputDecoration( - fillColor: _passwordsMatch ? _validFieldValueColor : null, + fillColor: _passwordsMatch && _passwordIsValid + ? _validFieldValueColor + : null, filled: true, hintText: "Confirm password", contentPadding: const EdgeInsets.symmetric( diff --git a/lib/ui/account/password_reentry_page.dart b/lib/ui/account/password_reentry_page.dart index e854157bc..c395d6cb4 100644 --- a/lib/ui/account/password_reentry_page.dart +++ b/lib/ui/account/password_reentry_page.dart @@ -8,7 +8,7 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/ui/account/recovery_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/home_widget.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/email_util.dart'; diff --git a/lib/ui/account/two_factor_authentication_page.dart b/lib/ui/account/two_factor_authentication_page.dart index dade437bc..d41035a13 100644 --- a/lib/ui/account/two_factor_authentication_page.dart +++ b/lib/ui/account/two_factor_authentication_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/lifecycle_event_handler.dart'; -import "package:photos/utils/dialog_util.dart"; import 'package:pinput/pin_put/pin_put.dart'; class TwoFactorAuthenticationPage extends StatefulWidget { @@ -124,11 +123,7 @@ class _TwoFactorAuthenticationPageState GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - showErrorDialog( - context, - "Contact support", - "Please drop an email to support@ente.io from your registered email address", - ); + UserService.instance.recoverTwoFactor(context, widget.sessionID); }, child: Container( padding: const EdgeInsets.all(10), diff --git a/lib/ui/account/two_factor_recovery_page.dart b/lib/ui/account/two_factor_recovery_page.dart new file mode 100644 index 000000000..acb2ba206 --- /dev/null +++ b/lib/ui/account/two_factor_recovery_page.dart @@ -0,0 +1,110 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:photos/services/user_service.dart'; +import 'package:photos/utils/dialog_util.dart'; + +class TwoFactorRecoveryPage extends StatefulWidget { + final String sessionID; + final String encryptedSecret; + final String secretDecryptionNonce; + + const TwoFactorRecoveryPage( + this.sessionID, + this.encryptedSecret, + this.secretDecryptionNonce, { + Key? key, + }) : super(key: key); + + @override + State createState() => _TwoFactorRecoveryPageState(); +} + +class _TwoFactorRecoveryPageState extends State { + final _recoveryKey = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + "Recover account", + style: TextStyle( + fontSize: 18, + ), + ), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(60, 0, 60, 0), + child: TextFormField( + decoration: const InputDecoration( + hintText: "Enter your recovery key", + contentPadding: EdgeInsets.all(20), + ), + style: const TextStyle( + fontSize: 14, + fontFeatures: [FontFeature.tabularFigures()], + ), + controller: _recoveryKey, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.multiline, + maxLines: null, + onChanged: (_) { + setState(() {}); + }, + ), + ), + const Padding(padding: EdgeInsets.all(24)), + Container( + padding: const EdgeInsets.fromLTRB(80, 0, 80, 0), + width: double.infinity, + height: 64, + child: OutlinedButton( + onPressed: _recoveryKey.text.isNotEmpty + ? () async { + await UserService.instance.removeTwoFactor( + context, + widget.sessionID, + _recoveryKey.text, + widget.encryptedSecret, + widget.secretDecryptionNonce, + ); + } + : null, + child: const Text("Recover"), + ), + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + showErrorDialog( + context, + "Contact support", + "Please drop an email to support@ente.io from your registered email address", + ); + }, + child: Container( + padding: const EdgeInsets.all(40), + child: Center( + child: Text( + "No recovery key?", + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + color: Colors.white.withOpacity(0.9), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/account/verify_recovery_page.dart b/lib/ui/account/verify_recovery_page.dart index 095c3c5d2..716be7993 100644 --- a/lib/ui/account/verify_recovery_page.dart +++ b/lib/ui/account/verify_recovery_page.dart @@ -12,7 +12,7 @@ import 'package:photos/services/user_remote_flag_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/common/gradient_button.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; diff --git a/lib/ui/actions/collection/collection_file_actions.dart b/lib/ui/actions/collection/collection_file_actions.dart index cc0353508..4f56b2167 100644 --- a/lib/ui/actions/collection/collection_file_actions.dart +++ b/lib/ui/actions/collection/collection_file_actions.dart @@ -6,7 +6,7 @@ import 'package:photos/services/favorites_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; diff --git a/lib/ui/actions/collection/collection_sharing_actions.dart b/lib/ui/actions/collection/collection_sharing_actions.dart index 436026331..bb4084ea9 100644 --- a/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/lib/ui/actions/collection/collection_sharing_actions.dart @@ -15,7 +15,7 @@ import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/payment/subscription.dart'; diff --git a/lib/ui/actions/file/file_actions.dart b/lib/ui/actions/file/file_actions.dart index d8465b228..e289e8e6f 100644 --- a/lib/ui/actions/file/file_actions.dart +++ b/lib/ui/actions/file/file_actions.dart @@ -5,9 +5,9 @@ import "package:photos/models/file_type.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/action_sheet_widget.dart"; -import "package:photos/ui/components/button_widget.dart"; +import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/models/button_type.dart"; -import "package:photos/ui/viewer/file/file_info_widget.dart"; +import 'package:photos/ui/viewer/file/file_details_widget.dart'; import "package:photos/utils/delete_file_util.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/toast_util.dart"; @@ -136,7 +136,7 @@ Future showInfoSheet(BuildContext context, File file) async { return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: FileInfoWidget(file), + child: FileDetailsWidget(file), ); }, ); diff --git a/lib/ui/advanced_settings_screen.dart b/lib/ui/advanced_settings_screen.dart index 35cf72604..cadd886e7 100644 --- a/lib/ui/advanced_settings_screen.dart +++ b/lib/ui/advanced_settings_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; diff --git a/lib/ui/backup_settings_screen.dart b/lib/ui/backup_settings_screen.dart index a908b21d9..8521803f0 100644 --- a/lib/ui/backup_settings_screen.dart +++ b/lib/ui/backup_settings_screen.dart @@ -3,9 +3,9 @@ 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/buttons/icon_button_widget.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/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; diff --git a/lib/ui/collection_action_sheet.dart b/lib/ui/collection_action_sheet.dart index d631a1b09..109d361dd 100644 --- a/lib/ui/collection_action_sheet.dart +++ b/lib/ui/collection_action_sheet.dart @@ -14,7 +14,7 @@ import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/collections_list_widget.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/bottom_of_title_bar_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/components/text_input_widget.dart"; import 'package:photos/ui/components/title_bar_title_widget.dart'; @@ -153,7 +153,6 @@ class _CollectionActionSheetState extends State { child: TextInputWidget( hintText: "Album name", prefixIcon: Icons.search_rounded, - autoFocus: true, onChange: (value) { setState(() { _searchQuery = value; @@ -199,47 +198,46 @@ class _CollectionActionSheetState extends State { return Flexible( child: Padding( padding: const EdgeInsets.fromLTRB(16, 24, 4, 0), - child: Scrollbar( - thumbVisibility: true, - radius: const Radius.circular(2), - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: FutureBuilder( - future: _getCollectionsWithThumbnail(), - builder: (context, snapshot) { - if (snapshot.hasError) { - //Need to show an error on the UI here - return const SizedBox.shrink(); - } else if (snapshot.hasData) { - final collectionsWithThumbnail = - snapshot.data as List; - _removeIncomingCollections(collectionsWithThumbnail); - final shouldShowCreateAlbum = - widget.showOptionToCreateNewAlbum && _searchQuery.isEmpty; - final searchResults = _searchQuery.isNotEmpty - ? collectionsWithThumbnail - .where( - (element) => element.collection.name! - .toLowerCase() - .contains(_searchQuery), - ) - .toList() - : collectionsWithThumbnail; - return CollectionsListWidget( + child: FutureBuilder( + future: _getCollectionsWithThumbnail(), + builder: (context, snapshot) { + if (snapshot.hasError) { + //Need to show an error on the UI here + return const SizedBox.shrink(); + } else if (snapshot.hasData) { + final collectionsWithThumbnail = + snapshot.data as List; + _removeIncomingCollections(collectionsWithThumbnail); + final shouldShowCreateAlbum = + widget.showOptionToCreateNewAlbum && _searchQuery.isEmpty; + final searchResults = _searchQuery.isNotEmpty + ? collectionsWithThumbnail + .where( + (element) => element.collection.name! + .toLowerCase() + .contains(_searchQuery), + ) + .toList() + : collectionsWithThumbnail; + return Scrollbar( + thumbVisibility: true, + radius: const Radius.circular(2), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: CollectionsListWidget( searchResults, widget.actionType, - widget.showOptionToCreateNewAlbum, widget.selectedFiles, widget.sharedFiles, _searchQuery, shouldShowCreateAlbum, - ); - } else { - return const EnteLoadingWidget(); - } - }, - ), - ), + ), + ), + ); + } else { + return const EnteLoadingWidget(); + } + }, ), ), ); diff --git a/lib/ui/collections_gallery_widget.dart b/lib/ui/collections_gallery_widget.dart index dc066b889..def130c1f 100644 --- a/lib/ui/collections_gallery_widget.dart +++ b/lib/ui/collections_gallery_widget.dart @@ -21,7 +21,7 @@ import 'package:photos/ui/collections/section_title.dart'; import 'package:photos/ui/collections/trash_button_widget.dart'; import 'package:photos/ui/collections/uncat_collections_button_widget.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import "package:photos/ui/components/icon_button_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/viewer/actions/delete_empty_albums.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/utils/local_settings.dart'; diff --git a/lib/ui/collections_list_widget.dart b/lib/ui/collections_list_widget.dart index bd9b246c0..1a8f1112a 100644 --- a/lib/ui/collections_list_widget.dart +++ b/lib/ui/collections_list_widget.dart @@ -30,7 +30,6 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; class CollectionsListWidget extends StatelessWidget { final List collectionsWithThumbnail; final CollectionActionType actionType; - final bool showOptionToCreateNewAlbum; final SelectedFiles? selectedFiles; final List? sharedFiles; final String searchQuery; @@ -39,7 +38,6 @@ class CollectionsListWidget extends StatelessWidget { CollectionsListWidget( this.collectionsWithThumbnail, this.actionType, - this.showOptionToCreateNewAlbum, this.selectedFiles, this.sharedFiles, this.searchQuery, @@ -56,18 +54,15 @@ class CollectionsListWidget extends StatelessWidget { : selectedFiles?.files.length ?? 0; if (collectionsWithThumbnail.isEmpty) { + if (shouldShowCreateAlbum) { + return _getNewAlbumWidget(context, filesCount); + } return const EmptyState(); } return ListView.separated( itemBuilder: (context, index) { if (index == 0 && shouldShowCreateAlbum) { - return GestureDetector( - onTap: () async { - await _createNewAlbumOnTap(context, filesCount); - }, - behavior: HitTestBehavior.opaque, - child: const NewAlbumListItemWidget(), - ); + return _getNewAlbumWidget(context, filesCount); } final item = collectionsWithThumbnail[index - (shouldShowCreateAlbum ? 1 : 0)]; @@ -89,6 +84,16 @@ class CollectionsListWidget extends StatelessWidget { ); } + GestureDetector _getNewAlbumWidget(BuildContext context, int filesCount) { + return GestureDetector( + onTap: () async { + await _createNewAlbumOnTap(context, filesCount); + }, + behavior: HitTestBehavior.opaque, + child: const NewAlbumListItemWidget(), + ); + } + Future _createNewAlbumOnTap( BuildContext context, int filesCount, diff --git a/lib/ui/common/divider_with_padding.dart b/lib/ui/common/divider_with_padding.dart deleted file mode 100644 index a4929bb24..000000000 --- a/lib/ui/common/divider_with_padding.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class DividerWithPadding extends StatelessWidget { - final double left, top, right, bottom, thickness; - - const DividerWithPadding({ - Key? key, - this.left = 0, - this.top = 0, - this.right = 0, - this.bottom = 0, - this.thickness = 0.5, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.fromLTRB(left, top, right, bottom), - child: Divider( - thickness: thickness, - ), - ); - } -} diff --git a/lib/ui/common/loading_widget.dart b/lib/ui/common/loading_widget.dart index 3ad6df236..57401b6f4 100644 --- a/lib/ui/common/loading_widget.dart +++ b/lib/ui/common/loading_widget.dart @@ -3,17 +3,25 @@ import 'package:photos/theme/ente_theme.dart'; class EnteLoadingWidget extends StatelessWidget { final Color? color; - final bool is20pts; - const EnteLoadingWidget({this.is20pts = false, this.color, Key? key}) - : super(key: key); + final double size; + final double padding; + final Alignment alignment; + const EnteLoadingWidget({ + this.color, + this.size = 14, + this.padding = 5, + this.alignment = Alignment.center, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - return Center( + return Align( + alignment: alignment, child: Padding( - padding: EdgeInsets.all(is20pts ? 3 : 5), + padding: EdgeInsets.all(padding), child: SizedBox.fromSize( - size: const Size.square(14), + size: Size.square(size), child: CircularProgressIndicator( strokeWidth: 2, color: color ?? getEnteColorScheme(context).strokeBase, diff --git a/lib/ui/common/rename_dialog.dart b/lib/ui/common/rename_dialog.dart deleted file mode 100644 index 98e58f80f..000000000 --- a/lib/ui/common/rename_dialog.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:photos/utils/dialog_util.dart'; - -class RenameDialog extends StatefulWidget { - final String? name; - final String type; - final int maxLength; - - const RenameDialog(this.name, this.type, {Key? key, this.maxLength = 100}) - : super(key: key); - - @override - State createState() => _RenameDialogState(); -} - -class _RenameDialogState extends State { - String? _newName; - - @override - void initState() { - super.initState(); - _newName = widget.name; - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text("Enter a new name"), - content: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - decoration: InputDecoration( - hintText: '${widget.type} name', - hintStyle: const TextStyle( - color: Colors.white30, - ), - contentPadding: const EdgeInsets.all(12), - ), - onChanged: (value) { - setState(() { - _newName = value; - }); - }, - autocorrect: false, - keyboardType: TextInputType.text, - initialValue: _newName, - autofocus: true, - ), - ], - ), - ), - actions: [ - TextButton( - child: const Text( - "Cancel", - style: TextStyle( - color: Colors.redAccent, - ), - ), - onPressed: () { - Navigator.of(context).pop(null); - }, - ), - TextButton( - child: Text( - "Rename", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - onPressed: () { - if (_newName!.trim().isEmpty) { - showErrorDialog( - context, - "Empty name", - "${widget.type} name cannot be empty", - ); - return; - } - if (_newName!.trim().length > widget.maxLength) { - showErrorDialog( - context, - "Name too large", - "${widget.type} name should be less than ${widget.maxLength} characters", - ); - return; - } - Navigator.of(context).pop(_newName!.trim()); - }, - ), - ], - ); - } -} diff --git a/lib/ui/components/action_sheet_widget.dart b/lib/ui/components/action_sheet_widget.dart index c7703f858..066d7b41c 100644 --- a/lib/ui/components/action_sheet_widget.dart +++ b/lib/ui/components/action_sheet_widget.dart @@ -7,7 +7,7 @@ import "package:photos/models/search/button_result.dart"; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/effects.dart'; import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/utils/separators_util.dart'; enum ActionSheetType { diff --git a/lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart b/lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart index 184a15d89..4e03a182d 100644 --- a/lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart +++ b/lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart @@ -8,7 +8,7 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/theme/effects.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/bottom_action_bar/action_bar_widget.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; class BottomActionBarWidget extends StatelessWidget { final String? text; diff --git a/lib/ui/components/button_widget.dart b/lib/ui/components/buttons/button_widget.dart similarity index 99% rename from lib/ui/components/button_widget.dart rename to lib/ui/components/buttons/button_widget.dart index b35a9fe77..ee318f1d7 100644 --- a/lib/ui/components/button_widget.dart +++ b/lib/ui/components/buttons/button_widget.dart @@ -350,7 +350,7 @@ class _ButtonChildWidgetState extends State { }, ), EnteLoadingWidget( - is20pts: true, + padding: 3, color: loadingIconColor, ), ], diff --git a/lib/ui/components/buttons/chip_button_widget.dart b/lib/ui/components/buttons/chip_button_widget.dart new file mode 100644 index 000000000..20c492737 --- /dev/null +++ b/lib/ui/components/buttons/chip_button_widget.dart @@ -0,0 +1,54 @@ +import "package:flutter/material.dart"; +import "package:photos/theme/ente_theme.dart"; + +///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=8119%3A59513&t=gQa1to5jY89Qk1k7-4 +class ChipButtonWidget extends StatelessWidget { + final String? label; + final IconData? leadingIcon; + final VoidCallback? onTap; + final bool noChips; + const ChipButtonWidget( + this.label, { + this.leadingIcon, + this.onTap, + this.noChips = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap?.call, + child: Container( + width: noChips ? double.infinity : null, + decoration: BoxDecoration( + color: getEnteColorScheme(context).fillFaint, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + leadingIcon != null + ? Icon( + leadingIcon, + size: 17, + ) + : const SizedBox.shrink(), + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + label ?? "", + style: getEnteTextTheme(context).smallBold, + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/components/icon_button_widget.dart b/lib/ui/components/buttons/icon_button_widget.dart similarity index 91% rename from lib/ui/components/icon_button_widget.dart rename to lib/ui/components/buttons/icon_button_widget.dart index 2bee00ca1..c97db2e91 100644 --- a/lib/ui/components/icon_button_widget.dart +++ b/lib/ui/components/buttons/icon_button_widget.dart @@ -43,6 +43,7 @@ class _IconButtonWidgetState extends State { @override Widget build(BuildContext context) { + final bool hasPressedState = widget.onTap != null; final colorTheme = getEnteColorScheme(context); iconStateColor ?? (iconStateColor = widget.defaultColor ?? @@ -52,9 +53,9 @@ class _IconButtonWidgetState extends State { return widget.disableGestureDetector ? _iconButton(colorTheme) : GestureDetector( - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onTapCancel: _onTapCancel, + onTapDown: hasPressedState ? _onTapDown : null, + onTapUp: hasPressedState ? _onTapUp : null, + onTapCancel: hasPressedState ? _onTapCancel : null, onTap: widget.onTap, child: _iconButton(colorTheme), ); diff --git a/lib/ui/components/buttons/inline_button_widget.dart b/lib/ui/components/buttons/inline_button_widget.dart new file mode 100644 index 000000000..64a4e6eb4 --- /dev/null +++ b/lib/ui/components/buttons/inline_button_widget.dart @@ -0,0 +1,20 @@ +import "package:flutter/cupertino.dart"; +import "package:photos/theme/ente_theme.dart"; + +class InlineButtonWidget extends StatelessWidget { + final String label; + final VoidCallback? onTap; + final TextStyle? textStyle; + const InlineButtonWidget(this.label, this.onTap, {this.textStyle, super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap?.call, + child: Text( + label, + style: textStyle ?? getEnteTextTheme(context).smallMuted, + ), + ); + } +} diff --git a/lib/ui/components/dialog_widget.dart b/lib/ui/components/dialog_widget.dart index 7de7d45cb..581567423 100644 --- a/lib/ui/components/dialog_widget.dart +++ b/lib/ui/components/dialog_widget.dart @@ -7,7 +7,7 @@ import 'package:photos/models/typedefs.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/effects.dart'; import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/components/text_input_widget.dart'; import 'package:photos/utils/separators_util.dart'; diff --git a/lib/ui/components/divider_widget.dart b/lib/ui/components/divider_widget.dart index 1c27ad51c..de30ea04f 100644 --- a/lib/ui/components/divider_widget.dart +++ b/lib/ui/components/divider_widget.dart @@ -11,15 +11,21 @@ enum DividerType { class DividerWidget extends StatelessWidget { final DividerType dividerType; final Color bgColor; + final bool divColorHasBlur; + final EdgeInsets? padding; const DividerWidget({ required this.dividerType, this.bgColor = Colors.transparent, + this.divColorHasBlur = true, + this.padding, super.key, }); @override Widget build(BuildContext context) { - final dividerColor = getEnteColorScheme(context).blurStrokeFaint; + final dividerColor = divColorHasBlur + ? getEnteColorScheme(context).blurStrokeFaint + : getEnteColorScheme(context).strokeFaint; if (dividerType == DividerType.solid) { return Container( @@ -38,6 +44,7 @@ class DividerWidget extends StatelessWidget { return Container( color: bgColor, + padding: padding ?? EdgeInsets.zero, child: Row( children: [ SizedBox( diff --git a/lib/ui/components/home_header_widget.dart b/lib/ui/components/home_header_widget.dart index 82aa04500..62e747bc3 100644 --- a/lib/ui/components/home_header_widget.dart +++ b/lib/ui/components/home_header_widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/viewer/search/search_widget.dart'; class HomeHeaderWidget extends StatefulWidget { diff --git a/lib/ui/components/info_item_widget.dart b/lib/ui/components/info_item_widget.dart new file mode 100644 index 000000000..603899523 --- /dev/null +++ b/lib/ui/components/info_item_widget.dart @@ -0,0 +1,106 @@ +import "package:flutter/material.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; + +///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=8113-59605&t=OMX5f5KdDJYWSQQN-4 +class InfoItemWidget extends StatelessWidget { + final IconData leadingIcon; + final VoidCallback? editOnTap; + final String? title; + final Future> subtitleSection; + final bool hasChipButtons; + const InfoItemWidget({ + required this.leadingIcon, + this.editOnTap, + this.title, + required this.subtitleSection, + this.hasChipButtons = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + final children = []; + if (title != null) { + children.addAll([ + Text( + title!, + style: hasChipButtons + ? getEnteTextTheme(context).smallMuted + : getEnteTextTheme(context).body, + ), + SizedBox(height: hasChipButtons ? 8 : 4), + ]); + } + + children.addAll([ + Flexible( + child: FutureBuilder( + future: subtitleSection, + builder: (context, snapshot) { + Widget child; + if (snapshot.hasData) { + final subtitle = snapshot.data as List; + if (subtitle.isNotEmpty) { + child = Wrap( + runSpacing: 8, + spacing: 8, + children: subtitle, + ); + } else { + child = const SizedBox.shrink(); + } + } else { + child = EnteLoadingWidget( + padding: 3, + size: 11, + color: getEnteColorScheme(context).strokeMuted, + alignment: Alignment.centerLeft, + ); + } + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeInOutExpo, + child: child, + ); + }, + ), + ), + ]); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButtonWidget( + icon: leadingIcon, + iconButtonType: IconButtonType.secondary, + ), + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 3.5, 16, 3.5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + ), + ], + ), + ), + editOnTap != null + ? IconButtonWidget( + icon: Icons.edit, + iconButtonType: IconButtonType.secondary, + onTap: editOnTap, + ) + : const SizedBox.shrink(), + ], + ); + } +} diff --git a/lib/ui/components/models/button_type.dart b/lib/ui/components/models/button_type.dart index f97bc9544..87d3c0dd3 100644 --- a/lib/ui/components/models/button_type.dart +++ b/lib/ui/components/models/button_type.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/text_style.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; enum ButtonType { primary, diff --git a/lib/ui/components/notification_widget.dart b/lib/ui/components/notification_widget.dart index cc9d7ec44..a6d23ce3f 100644 --- a/lib/ui/components/notification_widget.dart +++ b/lib/ui/components/notification_widget.dart @@ -3,7 +3,7 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/theme/colors.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/theme/text_style.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; // CreateNotificationType enum enum NotificationType { diff --git a/lib/ui/components/text_input_widget.dart b/lib/ui/components/text_input_widget.dart index c71b03f9c..53ae63521 100644 --- a/lib/ui/components/text_input_widget.dart +++ b/lib/ui/components/text_input_widget.dart @@ -77,9 +77,11 @@ class _TextInputWidgetState extends State { selection: TextSelection.collapsed(offset: widget.initialValue!.length), ); } - _textController.addListener(() { - widget.onChange!.call(_textController.text); - }); + if (widget.onChange != null) { + _textController.addListener(() { + widget.onChange!.call(_textController.text); + }); + } _obscureTextNotifier = ValueNotifier(widget.isPasswordInput); _obscureTextNotifier.addListener(_safeRefresh); super.initState(); diff --git a/lib/ui/components/title_bar_widget.dart b/lib/ui/components/title_bar_widget.dart index fe5402817..25a2cf2d6 100644 --- a/lib/ui/components/title_bar_widget.dart +++ b/lib/ui/components/title_bar_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; class TitleBarWidget extends StatelessWidget { final IconButtonWidget? leading; diff --git a/lib/ui/growth/apply_code_screen.dart b/lib/ui/growth/apply_code_screen.dart index f9a3029e4..9157479c5 100644 --- a/lib/ui/growth/apply_code_screen.dart +++ b/lib/ui/growth/apply_code_screen.dart @@ -5,8 +5,8 @@ import "package:photos/models/api/storage_bonus/storage_bonus.dart"; import "package:photos/models/user_details.dart"; import "package:photos/services/storage_bonus_service.dart"; import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/components/button_widget.dart"; -import "package:photos/ui/components/icon_button_widget.dart"; +import 'package:photos/ui/components/buttons/button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; @@ -135,9 +135,10 @@ class _ApplyCodeScreenState extends State { Logger('$runtimeType') .severe("failed to apply referral", e); showErrorDialogForException( - context: context, - exception: e as Exception, - apiErrorPrefix: "Failed to apply code"); + context: context, + exception: e as Exception, + apiErrorPrefix: "Failed to apply code", + ); } }, ) diff --git a/lib/ui/growth/code_success_screen.dart b/lib/ui/growth/code_success_screen.dart index 52b93daac..0534137a2 100644 --- a/lib/ui/growth/code_success_screen.dart +++ b/lib/ui/growth/code_success_screen.dart @@ -3,8 +3,8 @@ import "package:flutter_animate/flutter_animate.dart"; import "package:photos/models/api/storage_bonus/storage_bonus.dart"; import "package:photos/models/user_details.dart"; import "package:photos/theme/ente_theme.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/captioned_text_widget.dart"; -import "package:photos/ui/components/icon_button_widget.dart"; import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; diff --git a/lib/ui/growth/referral_screen.dart b/lib/ui/growth/referral_screen.dart index cec29bf01..637d1a24b 100644 --- a/lib/ui/growth/referral_screen.dart +++ b/lib/ui/growth/referral_screen.dart @@ -6,9 +6,9 @@ import "package:photos/services/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/common/web_page.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.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/menu_item_widget.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; @@ -43,7 +43,7 @@ class _ReferralScreenState extends State { await UserService.instance.getUserDetailsV2(memoryCount: false); final referralView = await StorageBonusService.instance.getGateway().getReferralView(); - return Tuple2(referralView, cachedUserDetails!); + return Tuple2(referralView, cachedUserDetails); } @override diff --git a/lib/ui/growth/storage_details_screen.dart b/lib/ui/growth/storage_details_screen.dart index 02144f4c0..c76fd3022 100644 --- a/lib/ui/growth/storage_details_screen.dart +++ b/lib/ui/growth/storage_details_screen.dart @@ -6,7 +6,7 @@ import "package:photos/models/user_details.dart"; import "package:photos/services/storage_bonus_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/components/icon_button_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; import "package:photos/utils/data_util.dart"; diff --git a/lib/ui/home/landing_page_widget.dart b/lib/ui/home/landing_page_widget.dart index 6e03e7257..a0a5eda06 100644 --- a/lib/ui/home/landing_page_widget.dart +++ b/lib/ui/home/landing_page_widget.dart @@ -10,7 +10,7 @@ import 'package:photos/ui/account/login_page.dart'; import 'package:photos/ui/account/password_entry_page.dart'; import 'package:photos/ui/account/password_reentry_page.dart'; import 'package:photos/ui/common/gradient_button.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/payment/subscription.dart'; diff --git a/lib/ui/home/memories_widget.dart b/lib/ui/home/memories_widget.dart index d011040be..fb3feeb2b 100644 --- a/lib/ui/home/memories_widget.dart +++ b/lib/ui/home/memories_widget.dart @@ -2,9 +2,9 @@ import "dart:io"; import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/memory.dart'; import 'package:photos/services/memories_service.dart'; +import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/actions/file/file_actions.dart"; import "package:photos/ui/extents_page_view.dart"; @@ -120,10 +120,7 @@ class _MemoryWidgetState extends State { type: MaterialType.transparency, child: Text( title, - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith(fontSize: 12), + style: getEnteTextTheme(context).mini, textAlign: TextAlign.center, ), ), @@ -136,22 +133,21 @@ class _MemoryWidgetState extends State { } Container _buildMemoryItem(BuildContext context, int index) { + final colorScheme = getEnteColorScheme(context); final memory = widget.memories[index]; final isSeen = memory.isSeen(); return Container( decoration: BoxDecoration( - border: isSeen - ? const Border() - : Border.all( - color: Theme.of(context).colorScheme.greenAlternative, - width: isSeen ? 0 : 2, - ), + border: Border.all( + color: isSeen ? colorScheme.strokeFaint : colorScheme.primary500, + width: 2, + ), borderRadius: BorderRadius.circular(40), ), child: ClipOval( child: SizedBox( - width: isSeen ? 60 : 56, - height: isSeen ? 60 : 56, + width: 56, + height: 56, child: Hero( tag: "memories" + memory.file.tag, child: ThumbnailWidget( diff --git a/lib/ui/home/status_bar_widget.dart b/lib/ui/home/status_bar_widget.dart index c5fc0c532..ca7346a23 100644 --- a/lib/ui/home/status_bar_widget.dart +++ b/lib/ui/home/status_bar_widget.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import "package:logging/logging.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/events/notification_event.dart'; @@ -24,6 +25,8 @@ class StatusBarWidget extends StatefulWidget { } class _StatusBarWidgetState extends State { + static final _logger = Logger("StatusBarWidget"); + late StreamSubscription _subscription; late StreamSubscription _notificationSubscription; bool _showStatus = false; @@ -33,6 +36,7 @@ class _StatusBarWidgetState extends State { @override void initState() { _subscription = Bus.instance.on().listen((event) { + _logger.info("Received event " + event.toString()); if (event.status == SyncStatus.error) { setState(() { _syncError = event.error; diff --git a/lib/ui/new_shared_collections_gallery.dart b/lib/ui/new_shared_collections_gallery.dart index 28b0183e3..9c7f366c6 100644 --- a/lib/ui/new_shared_collections_gallery.dart +++ b/lib/ui/new_shared_collections_gallery.dart @@ -2,7 +2,7 @@ import "package:flutter/material.dart"; import "package:photos/core/constants.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/collection_action_sheet.dart"; -import "package:photos/ui/components/button_widget.dart"; +import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/empty_state_item_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/utils/share_util.dart"; diff --git a/lib/ui/notification/update/change_log_page.dart b/lib/ui/notification/update/change_log_page.dart index 7e60d59e3..16da82f19 100644 --- a/lib/ui/notification/update/change_log_page.dart +++ b/lib/ui/notification/update/change_log_page.dart @@ -3,7 +3,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/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; diff --git a/lib/ui/payment/child_subscription_widget.dart b/lib/ui/payment/child_subscription_widget.dart index 1be85c8dc..c27759074 100644 --- a/lib/ui/payment/child_subscription_widget.dart +++ b/lib/ui/payment/child_subscription_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/models/user_details.dart'; import 'package:photos/services/user_service.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/utils/dialog_util.dart'; class ChildSubscriptionWidget extends StatelessWidget { diff --git a/lib/ui/payment/store_subscription_page.dart b/lib/ui/payment/store_subscription_page.dart index f8cb1fa1d..8f0dd772f 100644 --- a/lib/ui/payment/store_subscription_page.dart +++ b/lib/ui/payment/store_subscription_page.dart @@ -384,12 +384,6 @@ class _StoreSubscriptionPageState extends State { margin: const EdgeInsets.only(bottom: 6), child: Column( children: [ - _isFreePlanUser() - ? Text( - "2 months free on yearly plans", - style: getEnteTextTheme(context).miniMuted, - ) - : const SizedBox.shrink(), RepaintBoundary( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -405,10 +399,17 @@ class _StoreSubscriptionPageState extends State { await _filterStorePlansForUi(); }, ), - planText("Yearly", !showYearlyPlan) + planText("Yearly", !showYearlyPlan), ], ), ), + _isFreePlanUser() + ? Text( + "2 months free on yearly plans", + style: getEnteTextTheme(context).miniMuted, + ) + : const SizedBox.shrink(), + const Padding(padding: EdgeInsets.all(8)), ], ), ); diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 1525551f1..18a9d4d16 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; +import "package:logging/logging.dart"; import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; @@ -14,7 +15,7 @@ import 'package:photos/ui/common/bottom_shadow.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/common/web_page.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/captioned_text_widget.dart"; import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart"; import 'package:photos/ui/payment/child_subscription_widget.dart'; @@ -55,6 +56,7 @@ class _StripeSubscriptionPageState extends State { bool _isStripeSubscriber = false; bool _showYearlyPlan = false; EnteColorScheme colorScheme = darkScheme; + final Logger logger = Logger("StripeSubscriptionPage"); @override void initState() { @@ -366,20 +368,44 @@ class _StripeSubscriptionPageState extends State { ); } - Future toggleStripeSubscription(bool isRenewCancelled) async { + // toggleStripeSubscription, based on current auto renew status, will + // toggle the auto renew status of the user's subscription + Future toggleStripeSubscription(bool isAutoRenewDisabled) async { await _dialog.show(); try { - isRenewCancelled + isAutoRenewDisabled ? await _billingService.activateStripeSubscription() : await _billingService.cancelStripeSubscription(); await _fetchSub(); } catch (e) { showShortToast( context, - isRenewCancelled ? 'Failed to renew' : 'Failed to cancel', + isAutoRenewDisabled ? 'Failed to renew' : 'Failed to cancel', ); } await _dialog.hide(); + if (!isAutoRenewDisabled && mounted) { + await showTextInputDialog( + context, + title: "Your subscription was cancelled. Would you like to share the " + "reason?", + submitButtonLabel: "Send", + hintText: "Optional, as short as you like...", + alwaysShowSuccessState: true, + textCapitalization: TextCapitalization.words, + onSubmit: (String text) async { + // indicates user cancelled the rename request + if (text == "" || text.trim().isEmpty) { + return; + } + try { + await UserService.instance.sendFeedback(context, text); + } catch (e, s) { + logger.severe("Failed to send feedback", e, s); + } + }, + ); + } } List _getStripePlanWidgets() { @@ -492,12 +518,6 @@ class _StripeSubscriptionPageState extends State { margin: const EdgeInsets.only(bottom: 6), child: Column( children: [ - _isFreePlanUser() - ? Text( - "2 months free on yearly plans", - style: getEnteTextTheme(context).miniMuted, - ) - : const SizedBox.shrink(), RepaintBoundary( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -513,10 +533,17 @@ class _StripeSubscriptionPageState extends State { await _filterStripeForUI(); }, ), - planText("Yearly", !_showYearlyPlan) + planText("Yearly", !_showYearlyPlan), ], ), ), + _isFreePlanUser() + ? Text( + "2 months free on yearly plans", + style: getEnteTextTheme(context).miniMuted, + ) + : const SizedBox.shrink(), + const Padding(padding: EdgeInsets.all(8)), ], ), ); diff --git a/lib/ui/settings/account_section_widget.dart b/lib/ui/settings/account_section_widget.dart index 278f623ac..b1b7a829d 100644 --- a/lib/ui/settings/account_section_widget.dart +++ b/lib/ui/settings/account_section_widget.dart @@ -7,14 +7,12 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/account/change_email_dialog.dart'; import 'package:photos/ui/account/delete_account_page.dart'; import 'package:photos/ui/account/password_entry_page.dart'; -import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import "package:photos/ui/payment/subscription.dart"; import 'package:photos/ui/settings/common_settings.dart'; -import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/navigation_util.dart'; import "package:url_launcher/url_launcher_string.dart"; class AccountSectionWidget extends StatelessWidget { @@ -35,38 +33,13 @@ class AccountSectionWidget extends StatelessWidget { sectionOptionSpacing, MenuItemWidget( captionedTextWidget: const CaptionedTextWidget( - title: "Recovery key", + title: "Manage subscription", ), pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - showOnlyLoadingState: true, onTap: () async { - final hasAuthenticated = await LocalAuthenticationService.instance - .requestLocalAuthentication( - context, - "Please authenticate to view your recovery key", - ); - if (hasAuthenticated) { - String recoveryKey; - try { - recoveryKey = await _getOrCreateRecoveryKey(context); - } catch (e) { - await showGenericErrorDialog(context: context); - return; - } - unawaited( - routeToPage( - context, - RecoveryKeyPage( - recoveryKey, - "OK", - showAppBar: true, - onDone: () {}, - ), - ), - ); - } + _onManageSubscriptionTapped(context); }, ), sectionOptionSpacing, @@ -157,7 +130,22 @@ class AccountSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - routeToPage(context, const DeleteAccountPage()); + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + "Please authenticate to initiate account deletion", + ); + if (hasAuthenticated) { + unawaited( + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const DeleteAccountPage(); + }, + ), + ), + ); + } }, ), sectionOptionSpacing, @@ -165,12 +153,6 @@ class AccountSectionWidget extends StatelessWidget { ); } - Future _getOrCreateRecoveryKey(BuildContext context) async { - return CryptoUtil.bin2hex( - await UserService.instance.getOrCreateRecoveryKey(context), - ); - } - void _onLogoutTapped(BuildContext context) { showChoiceActionSheet( context, @@ -182,4 +164,14 @@ class AccountSectionWidget extends StatelessWidget { }, ); } + + void _onManageSubscriptionTapped(BuildContext context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return getSubscriptionPage(); + }, + ), + ); + } } diff --git a/lib/ui/settings/general_section_widget.dart b/lib/ui/settings/general_section_widget.dart index 7770913cd..e64486dbb 100644 --- a/lib/ui/settings/general_section_widget.dart +++ b/lib/ui/settings/general_section_widget.dart @@ -7,7 +7,6 @@ import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import "package:photos/ui/growth/referral_screen.dart"; -import 'package:photos/ui/payment/subscription.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/navigation_util.dart'; @@ -26,18 +25,6 @@ class GeneralSectionWidget extends StatelessWidget { Widget _getSectionOptions(BuildContext context) { return Column( children: [ - sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: const CaptionedTextWidget( - title: "Manage subscription", - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - _onManageSubscriptionTapped(context); - }, - ), sectionOptionSpacing, MenuItemWidget( captionedTextWidget: const CaptionedTextWidget( @@ -84,16 +71,6 @@ class GeneralSectionWidget extends StatelessWidget { ); } - void _onManageSubscriptionTapped(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return getSubscriptionPage(); - }, - ), - ); - } - Future _onFamilyPlansTapped(BuildContext context) async { final userDetails = await UserService.instance.getUserDetailsV2(memoryCount: false); diff --git a/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index e5c16452b..8c370d1eb 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/lib/ui/settings/security_section_widget.dart @@ -8,12 +8,16 @@ import 'package:photos/events/two_factor_status_change_event.dart'; import 'package:photos/services/local_authentication_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/ui/account/recovery_key_page.dart"; import 'package:photos/ui/account/sessions_page.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; +import "package:photos/utils/crypto_util.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/navigation_util.dart"; class SecuritySectionWidget extends StatefulWidget { const SecuritySectionWidget({Key? key}) : super(key: key); @@ -60,6 +64,43 @@ class _SecuritySectionWidgetState extends State { if (_config.hasConfiguredAccount()) { children.addAll( [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Recovery key", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + showOnlyLoadingState: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + "Please authenticate to view your recovery key", + ); + if (hasAuthenticated) { + String recoveryKey; + try { + recoveryKey = await _getOrCreateRecoveryKey(context); + } catch (e) { + await showGenericErrorDialog(context: context); + return; + } + unawaited( + routeToPage( + context, + RecoveryKeyPage( + recoveryKey, + "OK", + showAppBar: true, + onDone: () {}, + ), + ), + ); + } + }, + ), sectionOptionSpacing, MenuItemWidget( captionedTextWidget: const CaptionedTextWidget( @@ -186,4 +227,10 @@ class _SecuritySectionWidgetState extends State { }, ); } + + Future _getOrCreateRecoveryKey(BuildContext context) async { + return CryptoUtil.bin2hex( + await UserService.instance.getOrCreateRecoveryKey(context), + ); + } } diff --git a/lib/ui/sharing/add_partipant_page.dart b/lib/ui/sharing/add_partipant_page.dart index 51e73a5ff..94bd4464a 100644 --- a/lib/ui/sharing/add_partipant_page.dart +++ b/lib/ui/sharing/add_partipant_page.dart @@ -5,7 +5,7 @@ import 'package:photos/models/collection.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; diff --git a/lib/ui/sharing/manage_album_participant.dart b/lib/ui/sharing/manage_album_participant.dart index f3f0b49f7..9541aee4a 100644 --- a/lib/ui/sharing/manage_album_participant.dart +++ b/lib/ui/sharing/manage_album_participant.dart @@ -4,7 +4,7 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; diff --git a/lib/ui/sharing/verify_identity_dialog.dart b/lib/ui/sharing/verify_identity_dialog.dart index 650ab717b..6755333d5 100644 --- a/lib/ui/sharing/verify_identity_dialog.dart +++ b/lib/ui/sharing/verify_identity_dialog.dart @@ -10,7 +10,7 @@ import "package:photos/core/configuration.dart"; import "package:photos/services/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/components/button_widget.dart"; +import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/utils/share_util.dart"; diff --git a/lib/ui/tools/app_lock.dart b/lib/ui/tools/app_lock.dart index 86a7ea043..bdaa48b8b 100644 --- a/lib/ui/tools/app_lock.dart +++ b/lib/ui/tools/app_lock.dart @@ -32,11 +32,13 @@ class AppLock extends StatefulWidget { final Duration backgroundLockLatency; final ThemeData? darkTheme; final ThemeData? lightTheme; + final ThemeMode savedThemeMode; const AppLock({ Key? key, required this.builder, required this.lockScreen, + required this.savedThemeMode, this.enabled = true, this.backgroundLockLatency = const Duration(seconds: 0), this.darkTheme, @@ -103,7 +105,7 @@ class _AppLockState extends State with WidgetsBindingObserver { return MaterialApp( home: this.widget.enabled ? this._lockScreen : this.widget.builder(null), navigatorKey: _navigatorKey, - themeMode: ThemeMode.system, + themeMode: widget.savedThemeMode, theme: widget.lightTheme, darkTheme: widget.darkTheme, supportedLocales: AppLocalizations.supportedLocales, diff --git a/lib/ui/tools/debug/app_storage_viewer.dart b/lib/ui/tools/debug/app_storage_viewer.dart index 7022ff388..64d8368d1 100644 --- a/lib/ui/tools/debug/app_storage_viewer.dart +++ b/lib/ui/tools/debug/app_storage_viewer.dart @@ -7,8 +7,8 @@ import 'package:photos/core/cache/video_cache_manager.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; diff --git a/lib/ui/tools/editor/image_editor_page.dart b/lib/ui/tools/editor/image_editor_page.dart index c5ad847a3..8f2624a79 100644 --- a/lib/ui/tools/editor/image_editor_page.dart +++ b/lib/ui/tools/editor/image_editor_page.dart @@ -16,7 +16,7 @@ import 'package:photos/models/location.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/tools/editor/filtered_image.dart'; import 'package:photos/ui/viewer/file/detail_page.dart'; diff --git a/lib/ui/viewer/actions/delete_empty_albums.dart b/lib/ui/viewer/actions/delete_empty_albums.dart index 25c24189f..0ccb1bc05 100644 --- a/lib/ui/viewer/actions/delete_empty_albums.dart +++ b/lib/ui/viewer/actions/delete_empty_albums.dart @@ -5,7 +5,7 @@ import 'package:photos/models/collection.dart'; import 'package:photos/models/file.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; class DeleteEmptyAlbums extends StatefulWidget { diff --git a/lib/ui/viewer/actions/file_selection_actions_widget.dart b/lib/ui/viewer/actions/file_selection_actions_widget.dart index d219894f0..177e26bc1 100644 --- a/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -18,7 +18,7 @@ import 'package:photos/ui/collection_action_sheet.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/blur_menu_item_widget.dart'; import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/sharing/manage_links_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; @@ -126,7 +126,7 @@ class _FileSelectionActionWidgetState extends State { firstList.add( BlurMenuItemWidget( leadingIcon: Icons.link_outlined, - labelText: "Create link$suffix", + labelText: "Share link$suffix", menuItemColor: colorScheme.fillFaint, onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null, ), diff --git a/lib/ui/viewer/actions/file_selection_overlay_bar.dart b/lib/ui/viewer/actions/file_selection_overlay_bar.dart index 81f82c153..f4d6a9b5c 100644 --- a/lib/ui/viewer/actions/file_selection_overlay_bar.dart +++ b/lib/ui/viewer/actions/file_selection_overlay_bar.dart @@ -7,7 +7,7 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/collection_action_sheet.dart'; import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/viewer/actions/file_selection_actions_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/magic_util.dart'; diff --git a/lib/ui/viewer/file/collections_list_of_file_widget.dart b/lib/ui/viewer/file/collections_list_of_file_widget.dart deleted file mode 100644 index a57f210aa..000000000 --- a/lib/ui/viewer/file/collections_list_of_file_widget.dart +++ /dev/null @@ -1,68 +0,0 @@ -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/models/gallery_type.dart'; -import 'package:photos/services/collections_service.dart'; -import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/viewer/file/file_info_collection_widget.dart'; -import 'package:photos/ui/viewer/gallery/collection_page.dart'; -import 'package:photos/utils/navigation_util.dart'; - -class CollectionsListOfFileWidget extends StatelessWidget { - final Future> allCollectionIDsOfFile; - final int currentUserID; - - const CollectionsListOfFileWidget( - this.allCollectionIDsOfFile, - this.currentUserID, { - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: allCollectionIDsOfFile, - builder: (context, snapshot) { - if (snapshot.hasData) { - final Set collectionIDs = snapshot.data!; - final collections = []; - for (var collectionID in collectionIDs) { - 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: isHidden ? 'Hidden' : collections[index].name, - onTap: () { - if (isHidden) { - return; - } - routeToPage( - context, - CollectionPage( - CollectionWithThumbnail(collections[index], null), - appBarType: collections[index].isOwner(currentUserID) - ? GalleryType.ownedCollection - : GalleryType.sharedCollection, - ), - ); - }, - ); - }, - ); - } else if (snapshot.hasError) { - Logger("CollectionsListOfFile").info(snapshot.error); - return const SizedBox.shrink(); - } else { - return const EnteLoadingWidget(); - } - }, - ); - } -} diff --git a/lib/ui/viewer/file/device_folders_list_of_file_widget.dart b/lib/ui/viewer/file/device_folders_list_of_file_widget.dart deleted file mode 100644 index 27463c85c..000000000 --- a/lib/ui/viewer/file/device_folders_list_of_file_widget.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/viewer/file/file_info_collection_widget.dart'; - -class DeviceFoldersListOfFileWidget extends StatelessWidget { - final Future> allDeviceFoldersOfFile; - const DeviceFoldersListOfFileWidget(this.allDeviceFoldersOfFile, {Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: allDeviceFoldersOfFile, - builder: (context, snapshot) { - if (snapshot.hasData) { - final List deviceFolders = snapshot.data!.toList(); - return ListView.builder( - itemCount: deviceFolders.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - return FileInfoCollectionWidget( - name: deviceFolders[index], - onTap: () {}, - ); - }, - ); - } else if (snapshot.hasError) { - Logger("DeviceFoldersListOfFile").info(snapshot.error); - return const SizedBox.shrink(); - } else { - return const EnteLoadingWidget(); - } - }, - ); - } -} diff --git a/lib/ui/viewer/file/exif_info_dialog.dart b/lib/ui/viewer/file/exif_info_dialog.dart index 6bcd34231..991799c42 100644 --- a/lib/ui/viewer/file/exif_info_dialog.dart +++ b/lib/ui/viewer/file/exif_info_dialog.dart @@ -2,31 +2,34 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:photos/models/file.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/utils/exif_util.dart'; -class ExifInfoDialog extends StatefulWidget { +class ExifInfoDialog extends StatelessWidget { final File file; const ExifInfoDialog(this.file, {Key? key}) : super(key: key); - @override - State createState() => _ExifInfoDialogState(); -} - -class _ExifInfoDialogState extends State { @override Widget build(BuildContext context) { - final scrollController = ScrollController(); + final textTheme = getEnteTextTheme(context); return AlertDialog( - title: Text( - widget.file.title!, - style: Theme.of(context).textTheme.headline5, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "EXIF", + style: textTheme.h3Bold, + ), + Text( + file.title!, + style: textTheme.smallMuted, + ), + ], ), content: Scrollbar( - controller: scrollController, thumbVisibility: true, child: SingleChildScrollView( - controller: scrollController, child: _getInfo(), ), ), @@ -34,7 +37,7 @@ class _ExifInfoDialogState extends State { TextButton( child: Text( "Close", - style: Theme.of(context).textTheme.bodyText1, + style: textTheme.body, ), onPressed: () { Navigator.of(context, rootNavigator: true).pop('dialog'); @@ -46,7 +49,7 @@ class _ExifInfoDialogState extends State { Widget _getInfo() { return FutureBuilder( - future: getExif(widget.file), + future: getExif(file), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { final exif = snapshot.data; diff --git a/lib/ui/viewer/file/file_details_widget.dart b/lib/ui/viewer/file/file_details_widget.dart new file mode 100644 index 000000000..12c9a4b58 --- /dev/null +++ b/lib/ui/viewer/file/file_details_widget.dart @@ -0,0 +1,255 @@ +import "package:exif/exif.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:photos/core/configuration.dart"; +import "package:photos/models/file.dart"; +import "package:photos/models/file_type.dart"; +import "package:photos/services/feature_flag_service.dart"; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/divider_widget.dart"; +import 'package:photos/ui/components/title_bar_widget.dart'; +import 'package:photos/ui/viewer/file/file_caption_widget.dart'; +import "package:photos/ui/viewer/file_details/added_by_widget.dart"; +import "package:photos/ui/viewer/file_details/albums_item_widget.dart"; +import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart'; +import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart"; +import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart'; +import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart"; +import "package:photos/ui/viewer/file_details/objects_item_widget.dart"; +import "package:photos/utils/exif_util.dart"; + +class FileDetailsWidget extends StatefulWidget { + final File file; + const FileDetailsWidget( + this.file, { + Key? key, + }) : super(key: key); + + @override + State createState() => _FileDetailsWidgetState(); +} + +class _FileDetailsWidgetState extends State { + final ValueNotifier?> _exifNotifier = ValueNotifier(null); + final Map _exifData = { + "focalLength": null, + "fNumber": null, + "resolution": null, + "takenOnDevice": null, + "exposureTime": null, + "ISO": null, + "megaPixels": null + }; + + bool _isImage = false; + late int _currentUserID; + bool showExifListTile = false; + + @override + void initState() { + debugPrint('file_details_sheet initState'); + _currentUserID = Configuration.instance.getUserID()!; + _isImage = widget.file.fileType == FileType.image || + widget.file.fileType == FileType.livePhoto; + if (_isImage) { + _exifNotifier.addListener(() { + if (_exifNotifier.value != null) { + _generateExifForDetails(_exifNotifier.value!); + } + showExifListTile = _exifData["focalLength"] != null || + _exifData["fNumber"] != null || + _exifData["takenOnDevice"] != null || + _exifData["exposureTime"] != null || + _exifData["ISO"] != null; + }); + getExif(widget.file).then((exif) { + _exifNotifier.value = exif; + }); + } + super.initState(); + } + + @override + void dispose() { + _exifNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final file = widget.file; + final bool isFileOwner = + file.ownerID == null || file.ownerID == _currentUserID; + + //Make sure the bottom most tile is always the same one, that is it should + //not be rendered only if a condition is met. + final fileDetailsTiles = []; + fileDetailsTiles.add( + !widget.file.isUploaded || + (!isFileOwner && (widget.file.caption?.isEmpty ?? true)) + ? const SizedBox(height: 16) + : Padding( + padding: const EdgeInsets.only(top: 8, bottom: 24), + child: isFileOwner + ? FileCaptionWidget(file: widget.file) + : FileCaptionReadyOnly(caption: widget.file.caption!), + ), + ); + fileDetailsTiles.addAll([ + CreationTimeItem(file, _currentUserID), + const FileDetailsDivider(), + ValueListenableBuilder( + valueListenable: _exifNotifier, + builder: (context, _, __) => FilePropertiesItemWidget( + file, + _isImage, + _exifData, + _currentUserID, + ), + ), + const FileDetailsDivider(), + ]); + fileDetailsTiles.add( + ValueListenableBuilder( + valueListenable: _exifNotifier, + builder: (context, value, _) { + return showExifListTile + ? Column( + children: [ + BasicExifItemWidget(_exifData), + const FileDetailsDivider(), + ], + ) + : const SizedBox.shrink(); + }, + ), + ); + if (_isImage) { + fileDetailsTiles.addAll([ + ValueListenableBuilder( + valueListenable: _exifNotifier, + builder: (context, value, _) { + return Column( + children: [ + AllExifItemWidget(file, _exifNotifier.value), + const FileDetailsDivider(), + ], + ); + }, + ) + ]); + } + if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) { + fileDetailsTiles.addAll([ + ObjectsItemWidget(file), + const FileDetailsDivider(), + ]); + } + if (file.uploadedFileID != null && file.updationTime != null) { + fileDetailsTiles.addAll( + [ + BackedUpTimeItemWidget(file), + const FileDetailsDivider(), + ], + ); + } + fileDetailsTiles.add(AlbumsItemWidget(file, _currentUserID)); + + 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( + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + slivers: [ + TitleBarWidget( + isFlexibleSpaceDisabled: true, + title: "Details", + isOnTopOfScreen: false, + backgroundColor: getEnteColorScheme(context).backgroundElevated, + leading: IconButtonWidget( + icon: Icons.expand_more_outlined, + iconButtonType: IconButtonType.primary, + onTap: () => Navigator.pop(context), + ), + ), + SliverToBoxAdapter( + child: AddedByWidget( + widget.file, + _currentUserID, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return fileDetailsTiles[index]; + }, + childCount: fileDetailsTiles.length, + ), + ) + ], + ), + ), + ), + ); + } + + _generateExifForDetails(Map exif) { + if (exif["EXIF FocalLength"] != null) { + _exifData["focalLength"] = + (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator / + (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio) + .denominator; + } + + if (exif["EXIF FNumber"] != null) { + _exifData["fNumber"] = + (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator / + (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator; + } + final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"]; + final imageLength = exif["EXIF ExifImageLength"] ?? + exif["Image " + "ImageLength"]; + if (imageWidth != null && imageLength != null) { + _exifData["resolution"] = '$imageWidth x $imageLength'; + _exifData['megaPixels'] = + ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) / + 1000000) + .toStringAsFixed(1); + } else { + debugPrint("No image width/height"); + } + if (exif["Image Make"] != null && exif["Image Model"] != null) { + _exifData["takenOnDevice"] = + exif["Image Make"].toString() + " " + exif["Image Model"].toString(); + } + + if (exif["EXIF ExposureTime"] != null) { + _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString(); + } + if (exif["EXIF ISOSpeedRatings"] != null) { + _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString(); + } + } +} + +class FileDetailsDivider extends StatelessWidget { + const FileDetailsDivider({super.key}); + + @override + Widget build(BuildContext context) { + const dividerPadding = EdgeInsets.symmetric(vertical: 15.5); + return const DividerWidget( + dividerType: DividerType.menu, + divColorHasBlur: false, + padding: dividerPadding, + ); + } +} diff --git a/lib/ui/viewer/file/file_info_collection_widget.dart b/lib/ui/viewer/file/file_info_collection_widget.dart deleted file mode 100644 index 5c38100cd..000000000 --- a/lib/ui/viewer/file/file_info_collection_widget.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; - -class FileInfoCollectionWidget extends StatelessWidget { - final String? name; - final Function? onTap; - const FileInfoCollectionWidget({this.name, this.onTap, Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap as void Function()?, - child: Container( - margin: const EdgeInsets.only( - top: 10, - bottom: 18, - right: 8, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .inverseBackgroundColor - .withOpacity(0.025), - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - name!, - style: Theme.of(context).textTheme.subtitle2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - ); - } -} diff --git a/lib/ui/viewer/file/file_info_widget.dart b/lib/ui/viewer/file/file_info_widget.dart deleted file mode 100644 index 7a392e6d6..000000000 --- a/lib/ui/viewer/file/file_info_widget.dart +++ /dev/null @@ -1,536 +0,0 @@ -import "dart:async"; - -import "package:exif/exif.dart"; -import "package:flutter/cupertino.dart"; -import "package:flutter/material.dart"; -import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; -import 'package:path/path.dart' as path; -import 'package:photo_manager/photo_manager.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/file.dart"; -import "package:photos/models/file_type.dart"; -import 'package:photos/services/collections_service.dart'; -import "package:photos/services/feature_flag_service.dart"; -import "package:photos/services/location_service.dart"; -import 'package:photos/theme/ente_theme.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/file_caption_widget.dart'; -import "package:photos/ui/viewer/file/location_chip.dart"; -import "package:photos/ui/viewer/file/locations_list.dart"; -import "package:photos/ui/viewer/file/object_tags_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"; -import "package:photos/utils/magic_util.dart"; - -class FileInfoWidget extends StatefulWidget { - final File file; - - const FileInfoWidget( - this.file, { - Key? key, - }) : super(key: key); - - @override - State createState() => _FileInfoWidgetState(); -} - -class _FileInfoWidgetState extends State { - Map? _exif; - late LocationService locationService = LocationService.instance; - final Map _exifData = { - "focalLength": null, - "fNumber": null, - "resolution": null, - "takenOnDevice": null, - "exposureTime": null, - "ISO": null, - "megaPixels": null - }; - - bool _isImage = false; - int? _currentUserID; - - @override - void initState() { - debugPrint('file_info_dialog initState'); - _currentUserID = Configuration.instance.getUserID(); - _isImage = widget.file.fileType == FileType.image || - widget.file.fileType == FileType.livePhoto; - if (_isImage) { - getExif(widget.file).then((exif) { - if (mounted) { - setState(() { - _exif = exif; - }); - } - }); - } - super.initState(); - } - - @override - Widget build(BuildContext context) { - final file = widget.file; - final fileIsBackedup = file.uploadedFileID == null ? false : true; - final bool isFileOwner = - file.ownerID == null || file.ownerID == _currentUserID; - late Future> allCollectionIDsOfFile; - //Typing this as Future> as it would be easier to implement showing multiple device folders for a file in the future - final Future> allDeviceFoldersOfFile = - Future.sync(() => {file.deviceFolder ?? ''}); - if (fileIsBackedup) { - allCollectionIDsOfFile = FilesDB.instance.getAllCollectionIDsOfFile( - file.uploadedFileID!, - ); - } - final dateTime = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - final dateTimeForUpdationTime = - DateTime.fromMicrosecondsSinceEpoch(file.updationTime!); - - if (_isImage && _exif != null) { - _generateExifForDetails(_exif!); - } - final bool showExifListTile = _exifData["focalLength"] != null || - _exifData["fNumber"] != null || - _exifData["takenOnDevice"] != null || - _exifData["exposureTime"] != null || - _exifData["ISO"] != null; - final bool showDimension = - _exifData["resolution"] != null && _exifData["megaPixels"] != null; - final listTiles = [ - !widget.file.isUploaded || - (!isFileOwner && (widget.file.caption?.isEmpty ?? true)) - ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.only(top: 8, bottom: 4), - child: isFileOwner - ? FileCaptionWidget(file: widget.file) - : FileCaptionReadyOnly(caption: widget.file.caption!), - ), - ListTile( - horizontalTitleGap: 2, - leading: const Padding( - padding: EdgeInsets.only(top: 8), - child: Icon(Icons.calendar_today_rounded), - ), - title: Text( - getFullDate( - DateTime.fromMicrosecondsSinceEpoch(file.creationTime!), - ), - ), - subtitle: Text( - getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName, - style: Theme.of(context).textTheme.bodyText2!.copyWith( - color: Theme.of(context) - .colorScheme - .defaultTextColor - .withOpacity(0.5), - ), - ), - trailing: (widget.file.ownerID == null || - widget.file.ownerID == _currentUserID) && - widget.file.uploadedFileID != null - ? IconButton( - onPressed: () { - _showDateTimePicker(widget.file); - }, - icon: const Icon(Icons.edit), - ) - : const SizedBox.shrink(), - ), - ListTile( - horizontalTitleGap: 2, - leading: _isImage - ? const Padding( - padding: EdgeInsets.only(top: 8), - child: Icon( - Icons.image, - ), - ) - : const Padding( - padding: EdgeInsets.only(top: 8), - child: Icon( - Icons.video_camera_back, - size: 27, - ), - ), - title: Text( - path.basenameWithoutExtension(file.displayName) + - path.extension(file.displayName).toUpperCase(), - ), - subtitle: Wrap( - children: [ - showDimension - ? Text( - "${_exifData["megaPixels"]}MP " - "${_exifData["resolution"]} ", - ) - : const SizedBox.shrink(), - _getFileSize(), - (file.fileType == FileType.video) && - (file.localID != null || file.duration != 0) - ? Padding( - padding: const EdgeInsets.only(left: 8.0), - child: _getVideoDuration(), - ) - : const SizedBox.shrink(), - ], - ), - trailing: file.uploadedFileID == null || file.ownerID != _currentUserID - ? const SizedBox.shrink() - : IconButton( - onPressed: () async { - await editFilename(context, file); - setState(() {}); - }, - icon: const Icon(Icons.edit), - ), - ), - showExifListTile - ? ListTile( - horizontalTitleGap: 2, - leading: const Icon(Icons.camera_rounded), - title: Text(_exifData["takenOnDevice"] ?? "--"), - subtitle: Wrap( - children: [ - _exifData["fNumber"] != null - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: Text('ƒ/' + _exifData["fNumber"].toString()), - ) - : const SizedBox.shrink(), - _exifData["exposureTime"] != null - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: Text(_exifData["exposureTime"]), - ) - : const SizedBox.shrink(), - _exifData["focalLength"] != null - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: - Text(_exifData["focalLength"].toString() + "mm"), - ) - : const SizedBox.shrink(), - _exifData["ISO"] != null - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: Text("ISO" + _exifData["ISO"].toString()), - ) - : const SizedBox.shrink(), - ], - ), - ) - : null, - SizedBox( - height: 62, - child: ListTile( - horizontalTitleGap: 0, - leading: const Icon(Icons.folder_outlined), - title: fileIsBackedup - ? CollectionsListOfFileWidget( - allCollectionIDsOfFile, - _currentUserID!, - ) - : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile), - ), - ), - FeatureFlagService.instance.isInternalUserOrDebugBuild() - ? SizedBox( - height: 62, - child: ListTile( - horizontalTitleGap: 0, - leading: const Icon(Icons.image_search), - title: ObjectTagsWidget(file), - ), - ) - : null, - (file.uploadedFileID != null && file.updationTime != null) - ? ListTile( - horizontalTitleGap: 2, - leading: const Padding( - padding: EdgeInsets.only(top: 8), - child: Icon(Icons.cloud_upload_outlined), - ), - title: Text( - getFullDate( - DateTime.fromMicrosecondsSinceEpoch(file.updationTime!), - ), - ), - subtitle: Text( - getTimeIn12hrFormat(dateTimeForUpdationTime) + - " " + - dateTimeForUpdationTime.timeZoneName, - style: Theme.of(context).textTheme.bodyText2!.copyWith( - color: Theme.of(context) - .colorScheme - .defaultTextColor - .withOpacity(0.5), - ), - ), - ) - : null, - ListTile( - horizontalTitleGap: 2, - leading: Padding( - padding: const EdgeInsets.only(top: 8), - child: locationService.getLocationsByFileID(file.generatedID!).isEmpty - ? const Icon(Icons.add_location_alt_rounded) - : const Icon(Icons.location_on_rounded), - ), - title: Text( - locationService.getLocationsByFileID(file.generatedID!).isEmpty - ? "Add Location" - : "Locations", - ), - subtitle: - locationService.getLocationsByFileID(file.generatedID!).isEmpty - ? Text( - "group nearby photos", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context) - .colorScheme - .defaultTextColor - .withOpacity(0.5), - ), - ) - : locationChipList( - file.generatedID!, - context, - ), - trailing: - locationService.getLocationsByFileID(file.generatedID!).isEmpty - ? IconButton( - onPressed: () async { - unawaited( - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return LocationsList( - state: 1, - fileId: file.generatedID, - ); - }, - ), - ), - ); - }, - icon: const Icon(Icons.arrow_forward_ios), - ) - : const SizedBox.shrink(), - ), - _isImage ? RawExifListTileWidget(_exif, widget.file) : null, - ]; - - 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( - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - slivers: [ - TitleBarWidget( - isFlexibleSpaceDisabled: true, - title: "Details", - isOnTopOfScreen: false, - backgroundColor: getEnteColorScheme(context).backgroundElevated, - leading: IconButtonWidget( - icon: Icons.close_outlined, - iconButtonType: IconButtonType.primary, - onTap: () => Navigator.pop(context), - ), - ), - SliverToBoxAdapter(child: addedBy(widget.file)), - 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, - ), - ) - ], - ), - ), - ), - ); - } - - Widget addedBy(File file) { - if (file.uploadedFileID == null) { - return const SizedBox.shrink(); - } - String? addedBy; - if (file.ownerID == _currentUserID) { - if (file.pubMagicMetadata!.uploaderName != null) { - addedBy = file.pubMagicMetadata!.uploaderName; - } - } else { - final fileOwner = CollectionsService.instance - .getFileOwner(file.ownerID!, file.collectionID); - addedBy = fileOwner.email; - } - if (addedBy == null || addedBy.isEmpty) { - return const SizedBox.shrink(); - } - final enteTheme = Theme.of(context).colorScheme.enteTheme; - return Padding( - padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 16), - child: Text( - "Added by $addedBy", - style: enteTheme.textTheme.mini - .copyWith(color: enteTheme.colorScheme.textMuted), - ), - ); - } - - _generateExifForDetails(Map exif) { - if (exif["EXIF FocalLength"] != null) { - _exifData["focalLength"] = - (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator / - (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio) - .denominator; - } - - if (exif["EXIF FNumber"] != null) { - _exifData["fNumber"] = - (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator / - (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator; - } - final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"]; - final imageLength = exif["EXIF ExifImageLength"] ?? - exif["Image " - "ImageLength"]; - if (imageWidth != null && imageLength != null) { - _exifData["resolution"] = '$imageWidth x $imageLength'; - _exifData['megaPixels'] = - ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) / - 1000000) - .toStringAsFixed(1); - } else { - debugPrint("No image width/height"); - } - if (exif["Image Make"] != null && exif["Image Model"] != null) { - _exifData["takenOnDevice"] = - exif["Image Make"].toString() + " " + exif["Image Model"].toString(); - } - - if (exif["EXIF ExposureTime"] != null) { - _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString(); - } - if (exif["EXIF ISOSpeedRatings"] != null) { - _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString(); - } - } - - Widget _getFileSize() { - Future fileSizeFuture; - if (widget.file.fileSize != null) { - fileSizeFuture = Future.value(widget.file.fileSize); - } else { - fileSizeFuture = getFile(widget.file).then((f) => f!.length()); - } - return FutureBuilder( - future: fileSizeFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text( - (snapshot.data! / (1024 * 1024)).toStringAsFixed(2) + " MB", - ); - } else { - return Center( - child: SizedBox.fromSize( - size: const Size.square(24), - child: const CupertinoActivityIndicator( - radius: 8, - ), - ), - ); - } - }, - ); - } - - Widget _getVideoDuration() { - if (widget.file.duration != 0) { - return Text( - secondsToHHMMSS(widget.file.duration!), - ); - } - return FutureBuilder( - future: widget.file.getAsset, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text( - snapshot.data!.videoDuration.toString().split(".")[0], - ); - } else { - return Center( - child: SizedBox.fromSize( - size: const Size.square(24), - child: const CupertinoActivityIndicator( - radius: 8, - ), - ), - ); - } - }, - ); - } - - void _showDateTimePicker(File file) async { - final dateResult = await DatePicker.showDatePicker( - context, - minTime: DateTime(1800, 1, 1), - maxTime: DateTime.now(), - currentTime: DateTime.fromMicrosecondsSinceEpoch(file.creationTime!), - locale: LocaleType.en, - theme: Theme.of(context).colorScheme.dateTimePickertheme, - ); - if (dateResult == null) { - return; - } - final dateWithTimeResult = await DatePicker.showTime12hPicker( - context, - showTitleActions: true, - currentTime: dateResult, - locale: LocaleType.en, - theme: Theme.of(context).colorScheme.dateTimePickertheme, - ); - if (dateWithTimeResult != null) { - if (await editTime( - context, - List.of([widget.file]), - dateWithTimeResult.microsecondsSinceEpoch, - )) { - widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch; - setState(() {}); - } - } - } -} diff --git a/lib/ui/viewer/file/object_tags_widget.dart b/lib/ui/viewer/file/object_tags_widget.dart deleted file mode 100644 index 4a24b4ac1..000000000 --- a/lib/ui/viewer/file/object_tags_widget.dart +++ /dev/null @@ -1,77 +0,0 @@ -import "package:flutter/material.dart"; -import "package:logging/logging.dart"; -import "package:photos/ente_theme_data.dart"; -import "package:photos/models/file.dart"; -import "package:photos/services/object_detection/object_detection_service.dart"; -import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/utils/thumbnail_util.dart"; - -class ObjectTagsWidget extends StatelessWidget { - final File file; - - const ObjectTagsWidget(this.file, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: getThumbnail(file).then((data) { - return ObjectDetectionService.instance.predict(data!); - }), - builder: (context, snapshot) { - if (snapshot.hasData) { - final List tags = snapshot.data!; - if (tags.isEmpty) { - return const ObjectTagWidget("No Results"); - } - return ListView.builder( - itemCount: tags.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - return ObjectTagWidget(tags[index]); - }, - ); - } else if (snapshot.hasError) { - Logger("ObjectTagsWidget").severe(snapshot.error); - return const Icon(Icons.error); - } else { - return const EnteLoadingWidget(); - } - }, - ); - } -} - -class ObjectTagWidget extends StatelessWidget { - final String name; - const ObjectTagWidget(this.name, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only( - top: 10, - bottom: 18, - right: 8, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .inverseBackgroundColor - .withOpacity(0.025), - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - name!, - style: Theme.of(context).textTheme.subtitle2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ); - } -} diff --git a/lib/ui/viewer/file/raw_exif_list_tile_widget.dart b/lib/ui/viewer/file/raw_exif_list_tile_widget.dart deleted file mode 100644 index 5cc71f3c3..000000000 --- a/lib/ui/viewer/file/raw_exif_list_tile_widget.dart +++ /dev/null @@ -1,69 +0,0 @@ -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? 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), - ), - ), - ), - ); - } -} diff --git a/lib/ui/viewer/file_details/added_by_widget.dart b/lib/ui/viewer/file_details/added_by_widget.dart new file mode 100644 index 000000000..a0ce60002 --- /dev/null +++ b/lib/ui/viewer/file_details/added_by_widget.dart @@ -0,0 +1,37 @@ +import "package:flutter/material.dart"; +import "package:photos/models/file.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/theme/ente_theme.dart"; + +class AddedByWidget extends StatelessWidget { + final File file; + final int currentUserID; + const AddedByWidget(this.file, this.currentUserID, {super.key}); + + @override + Widget build(BuildContext context) { + if (file.uploadedFileID == null) { + return const SizedBox.shrink(); + } + String? addedBy; + if (file.ownerID == currentUserID) { + if (file.pubMagicMetadata!.uploaderName != null) { + addedBy = file.pubMagicMetadata!.uploaderName; + } + } else { + final fileOwner = CollectionsService.instance + .getFileOwner(file.ownerID!, file.collectionID); + addedBy = fileOwner.email; + } + if (addedBy == null || addedBy.isEmpty) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 16), + child: Text( + "Added by $addedBy", + style: getEnteTextTheme(context).miniMuted, + ), + ); + } +} diff --git a/lib/ui/viewer/file_details/albums_item_widget.dart b/lib/ui/viewer/file_details/albums_item_widget.dart new file mode 100644 index 000000000..77974731e --- /dev/null +++ b/lib/ui/viewer/file_details/albums_item_widget.dart @@ -0,0 +1,109 @@ +import "package:flutter/material.dart"; +import "package:logging/logging.dart"; +import "package:photos/db/files_db.dart"; +import "package:photos/models/collection.dart"; +import "package:photos/models/collection_items.dart"; +import "package:photos/models/file.dart"; +import "package:photos/models/gallery_type.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/ui/components/buttons/chip_button_widget.dart"; +import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/ui/viewer/gallery/collection_page.dart"; +import "package:photos/utils/navigation_util.dart"; + +class AlbumsItemWidget extends StatelessWidget { + final File file; + final int currentUserID; + const AlbumsItemWidget( + this.file, + this.currentUserID, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final fileIsBackedup = file.uploadedFileID == null ? false : true; + late Future> allCollectionIDsOfFile; + //Typing this as Future> as it would be easier to implement showing multiple device folders for a file in the future + final Future> allDeviceFoldersOfFile = + Future.sync(() => {file.deviceFolder ?? ''}); + if (fileIsBackedup) { + allCollectionIDsOfFile = FilesDB.instance.getAllCollectionIDsOfFile( + file.uploadedFileID!, + ); + } + return InfoItemWidget( + key: const ValueKey("Albums"), + leadingIcon: Icons.folder_outlined, + title: "Albums", + subtitleSection: fileIsBackedup + ? _collectionsListOfFile( + context, + allCollectionIDsOfFile, + currentUserID, + ) + : _deviceFoldersListOfFile(allDeviceFoldersOfFile), + hasChipButtons: true, + ); + } + + Future> _deviceFoldersListOfFile( + Future> allDeviceFoldersOfFile, + ) async { + try { + final chipButtons = []; + final List deviceFolders = + (await allDeviceFoldersOfFile).toList(); + for (var deviceFolder in deviceFolders) { + chipButtons.add( + ChipButtonWidget( + deviceFolder, + ), + ); + } + return chipButtons; + } catch (e, s) { + Logger("AlbumsItemWidget").info(e, s); + return []; + } + } + + Future> _collectionsListOfFile( + BuildContext context, + Future> allCollectionIDsOfFile, + int currentUserID, + ) async { + try { + final chipButtons = []; + final Set collectionIDs = await allCollectionIDsOfFile; + final collections = []; + for (var collectionID in collectionIDs) { + final c = CollectionsService.instance.getCollectionByID(collectionID); + collections.add(c!); + chipButtons.add( + ChipButtonWidget( + c.isHidden() ? "Hidden" : c.name, + onTap: () { + if (c.isHidden()) { + return; + } + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail(c, null), + appBarType: c.isOwner(currentUserID) + ? GalleryType.ownedCollection + : GalleryType.sharedCollection, + ), + ); + }, + ), + ); + } + return chipButtons; + } catch (e, s) { + Logger("AlbumsItemWidget").info(e, s); + return []; + } + } +} diff --git a/lib/ui/viewer/file_details/backed_up_time_item_widget.dart b/lib/ui/viewer/file_details/backed_up_time_item_widget.dart new file mode 100644 index 000000000..4e7ffc3a9 --- /dev/null +++ b/lib/ui/viewer/file_details/backed_up_time_item_widget.dart @@ -0,0 +1,31 @@ +import "package:flutter/material.dart"; +import "package:photos/models/file.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/utils/date_time_util.dart"; + +class BackedUpTimeItemWidget extends StatelessWidget { + final File file; + const BackedUpTimeItemWidget(this.file, {super.key}); + + @override + Widget build(BuildContext context) { + final dateTimeForUpdationTime = + DateTime.fromMicrosecondsSinceEpoch(file.updationTime!); + return InfoItemWidget( + key: const ValueKey("Backedup date"), + leadingIcon: Icons.backup_outlined, + title: getFullDate( + DateTime.fromMicrosecondsSinceEpoch(file.updationTime!), + ), + subtitleSection: Future.value([ + Text( + getTimeIn12hrFormat(dateTimeForUpdationTime) + + " " + + dateTimeForUpdationTime.timeZoneName, + style: getEnteTextTheme(context).smallMuted, + ), + ]), + ); + } +} diff --git a/lib/ui/viewer/file_details/creation_time_item_widget.dart b/lib/ui/viewer/file_details/creation_time_item_widget.dart new file mode 100644 index 000000000..2f7e25271 --- /dev/null +++ b/lib/ui/viewer/file_details/creation_time_item_widget.dart @@ -0,0 +1,76 @@ +import "package:flutter/material.dart"; +import "package:flutter_datetime_picker/flutter_datetime_picker.dart"; +import "package:photos/ente_theme_data.dart"; +import "package:photos/models/file.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/utils/date_time_util.dart"; +import "package:photos/utils/magic_util.dart"; + +class CreationTimeItem extends StatefulWidget { + final File file; + final int currentUserID; + const CreationTimeItem(this.file, this.currentUserID, {super.key}); + + @override + State createState() => _CreationTimeItemState(); +} + +class _CreationTimeItemState extends State { + @override + Widget build(BuildContext context) { + final dateTime = + DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!); + return InfoItemWidget( + key: const ValueKey("Creation time"), + leadingIcon: Icons.calendar_today_outlined, + title: getFullDate( + DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!), + ), + subtitleSection: Future.value([ + Text( + getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName, + style: getEnteTextTheme(context).smallMuted, + ), + ]), + editOnTap: ((widget.file.ownerID == null || + widget.file.ownerID == widget.currentUserID) && + widget.file.uploadedFileID != null) + ? () { + _showDateTimePicker(widget.file); + } + : null, + ); + } + + void _showDateTimePicker(File file) async { + final dateResult = await DatePicker.showDatePicker( + context, + minTime: DateTime(1800, 1, 1), + maxTime: DateTime.now(), + currentTime: DateTime.fromMicrosecondsSinceEpoch(file.creationTime!), + locale: LocaleType.en, + theme: Theme.of(context).colorScheme.dateTimePickertheme, + ); + if (dateResult == null) { + return; + } + final dateWithTimeResult = await DatePicker.showTime12hPicker( + context, + showTitleActions: true, + currentTime: dateResult, + locale: LocaleType.en, + theme: Theme.of(context).colorScheme.dateTimePickertheme, + ); + if (dateWithTimeResult != null) { + if (await editTime( + context, + List.of([widget.file]), + dateWithTimeResult.microsecondsSinceEpoch, + )) { + widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch; + setState(() {}); + } + } + } +} diff --git a/lib/ui/viewer/file_details/exif_item_widgets.dart b/lib/ui/viewer/file_details/exif_item_widgets.dart new file mode 100644 index 000000000..87729a86e --- /dev/null +++ b/lib/ui/viewer/file_details/exif_item_widgets.dart @@ -0,0 +1,96 @@ +import "package:exif/exif.dart"; +import "package:flutter/material.dart"; +import "package:photos/models/file.dart"; +import "package:photos/theme/colors.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/buttons/inline_button_widget.dart"; +import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/ui/viewer/file/exif_info_dialog.dart"; +import "package:photos/utils/toast_util.dart"; + +class BasicExifItemWidget extends StatelessWidget { + final Map exifData; + const BasicExifItemWidget(this.exifData, {super.key}); + + @override + Widget build(BuildContext context) { + final subtitleTextTheme = getEnteTextTheme(context).smallMuted; + return InfoItemWidget( + key: const ValueKey("Basic EXIF"), + leadingIcon: Icons.camera_outlined, + title: exifData["takenOnDevice"] ?? "--", + subtitleSection: Future.value([ + if (exifData["fNumber"] != null) + Text( + 'ƒ/' + exifData["fNumber"].toString(), + style: subtitleTextTheme, + ), + if (exifData["exposureTime"] != null) + Text( + exifData["exposureTime"], + style: subtitleTextTheme, + ), + if (exifData["focalLength"] != null) + Text( + exifData["focalLength"].toString() + "mm", + style: subtitleTextTheme, + ), + if (exifData["ISO"] != null) + Text( + "ISO" + exifData["ISO"].toString(), + style: subtitleTextTheme, + ), + ]), + ); + } +} + +class AllExifItemWidget extends StatelessWidget { + final File file; + final Map? exif; + const AllExifItemWidget( + this.file, + this.exif, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return InfoItemWidget( + leadingIcon: Icons.text_snippet_outlined, + title: "EXIF", + subtitleSection: _exifButton(context, file, exif), + ); + } + + Future> _exifButton( + BuildContext context, + File file, + Map? exif, + ) { + late final String label; + late final VoidCallback? onTap; + if (exif == null) { + label = "Loading EXIF data..."; + onTap = null; + } else if (exif.isNotEmpty) { + label = "View all EXIF data"; + onTap = () => showDialog( + context: context, + builder: (BuildContext context) { + return ExifInfoDialog(file); + }, + barrierColor: backdropFaintDark, + ); + } else { + label = "No EXIF data"; + onTap = () => showShortToast(context, "This image has no exif data"); + } + return Future.value([ + InlineButtonWidget( + label, + onTap, + ) + ]); + } +} diff --git a/lib/ui/viewer/file_details/file_properties_item_widget.dart b/lib/ui/viewer/file_details/file_properties_item_widget.dart new file mode 100644 index 000000000..688add62f --- /dev/null +++ b/lib/ui/viewer/file_details/file_properties_item_widget.dart @@ -0,0 +1,99 @@ +import "package:flutter/material.dart"; +import 'package:path/path.dart' as path; +import "package:photos/models/file.dart"; +import "package:photos/models/file_type.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/utils/date_time_util.dart"; +import "package:photos/utils/file_util.dart"; +import "package:photos/utils/magic_util.dart"; + +class FilePropertiesItemWidget extends StatefulWidget { + final File file; + final bool isImage; + final Map exifData; + final int currentUserID; + const FilePropertiesItemWidget( + this.file, + this.isImage, + this.exifData, + this.currentUserID, { + super.key, + }); + @override + State createState() => + _FilePropertiesItemWidgetState(); +} + +class _FilePropertiesItemWidgetState extends State { + @override + Widget build(BuildContext context) { + return InfoItemWidget( + key: const ValueKey("File properties"), + leadingIcon: widget.isImage + ? Icons.photo_outlined + : Icons.video_camera_back_outlined, + title: path.basenameWithoutExtension(widget.file.displayName) + + path.extension(widget.file.displayName).toUpperCase(), + subtitleSection: _subTitleSection(), + editOnTap: widget.file.uploadedFileID == null || + widget.file.ownerID != widget.currentUserID + ? null + : () async { + await editFilename(context, widget.file); + setState(() {}); + }, + ); + } + + Future> _subTitleSection() async { + final bool showDimension = widget.exifData["resolution"] != null && + widget.exifData["megaPixels"] != null; + final subSectionWidgets = []; + + if (showDimension) { + subSectionWidgets.add( + Text( + "${widget.exifData["megaPixels"]}MP " + "${widget.exifData["resolution"]} ", + style: getEnteTextTheme(context).smallMuted, + ), + ); + } + + int fileSize; + if (widget.file.fileSize != null) { + fileSize = widget.file.fileSize!; + } else { + fileSize = await getFile(widget.file).then((f) => f!.length()); + } + subSectionWidgets.add( + Text( + (fileSize / (1024 * 1024)).toStringAsFixed(2) + " MB", + style: getEnteTextTheme(context).smallMuted, + ), + ); + + if ((widget.file.fileType == FileType.video) && + (widget.file.localID != null || widget.file.duration != 0)) { + if (widget.file.duration != 0) { + subSectionWidgets.add( + Text( + secondsToHHMMSS(widget.file.duration!), + style: getEnteTextTheme(context).smallMuted, + ), + ); + } else { + final asset = await widget.file.getAsset; + subSectionWidgets.add( + Text( + asset?.videoDuration.toString().split(".")[0] ?? "", + style: getEnteTextTheme(context).smallMuted, + ), + ); + } + } + + return Future.value(subSectionWidgets); + } +} diff --git a/lib/ui/viewer/file_details/objects_item_widget.dart b/lib/ui/viewer/file_details/objects_item_widget.dart new file mode 100644 index 000000000..4b1de7177 --- /dev/null +++ b/lib/ui/viewer/file_details/objects_item_widget.dart @@ -0,0 +1,48 @@ +import "package:flutter/material.dart"; +import "package:logging/logging.dart"; +import "package:photos/models/file.dart"; +import "package:photos/services/object_detection/object_detection_service.dart"; +import "package:photos/ui/components/buttons/chip_button_widget.dart"; +import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/utils/thumbnail_util.dart"; + +class ObjectsItemWidget extends StatelessWidget { + final File file; + const ObjectsItemWidget(this.file, {super.key}); + + @override + Widget build(BuildContext context) { + return InfoItemWidget( + key: const ValueKey("Objects"), + leadingIcon: Icons.image_search_outlined, + subtitleSection: _objectTags(file), + hasChipButtons: true, + ); + } + + Future> _objectTags(File file) async { + try { + final chipButtons = []; + var objectTags = []; + final thumbnail = await getThumbnail(file); + if (thumbnail != null) { + objectTags = await ObjectDetectionService.instance.predict(thumbnail); + } + if (objectTags.isEmpty) { + return const [ + ChipButtonWidget( + "No results", + noChips: true, + ) + ]; + } + for (String objectTag in objectTags) { + chipButtons.add(ChipButtonWidget(objectTag)); + } + return chipButtons; + } catch (e, s) { + Logger("ObjctsItemWidget").info(e, s); + return []; + } + } +} diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 77c4b920a..75408bdee 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -18,7 +18,7 @@ import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/sharing/album_participants_page.dart'; diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index 4d2e18e18..1e4ffaac8 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -5,7 +5,7 @@ import 'package:logging/logging.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/search/search_result.dart'; import 'package:photos/services/search_service.dart'; -import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/viewer/search/result/no_result_widget.dart'; import 'package:photos/ui/viewer/search/search_suffix_icon_widget.dart'; import 'package:photos/ui/viewer/search/search_suggestions.dart'; diff --git a/lib/utils/auth_util.dart b/lib/utils/auth_util.dart index 808a88c09..77d1982b1 100644 --- a/lib/utils/auth_util.dart +++ b/lib/utils/auth_util.dart @@ -1,4 +1,3 @@ -import 'package:local_auth/auth_strings.dart'; import 'package:local_auth/local_auth.dart'; import 'package:logging/logging.dart'; @@ -7,18 +6,5 @@ Future requestAuthentication(String reason) async { await LocalAuthentication().stopAuthentication(); return await LocalAuthentication().authenticate( localizedReason: reason, - androidAuthStrings: const AndroidAuthMessages( - biometricHint: "Verify identity", - biometricNotRecognized: "Not recognized, try again", - biometricRequiredTitle: "Biometric required", - biometricSuccess: "Successfully verified", - cancelButton: "Cancel", - deviceCredentialsRequiredTitle: "Device credentials required", - deviceCredentialsSetupDescription: "Device credentials required", - goToSettingsButton: "Go to settings", - goToSettingsDescription: - "Authentication is not setup on your device, go to Settings > Security to set it up", - signInTitle: "Authentication required", - ), ); } diff --git a/lib/utils/delete_file_util.dart b/lib/utils/delete_file_util.dart index 15db93100..cecc0dcc2 100644 --- a/lib/utils/delete_file_util.dart +++ b/lib/utils/delete_file_util.dart @@ -23,7 +23,7 @@ import 'package:photos/services/sync_service.dart'; import 'package:photos/services/trash_sync_service.dart'; import 'package:photos/ui/common/linear_progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/file_util.dart'; diff --git a/lib/utils/dialog_util.dart b/lib/utils/dialog_util.dart index 59c9418f0..c0f0fc4ed 100644 --- a/lib/utils/dialog_util.dart +++ b/lib/utils/dialog_util.dart @@ -10,7 +10,7 @@ import 'package:photos/theme/colors.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; diff --git a/lib/utils/email_util.dart b/lib/utils/email_util.dart index c78195f8e..0454a8347 100644 --- a/lib/utils/email_util.dart +++ b/lib/utils/email_util.dart @@ -12,7 +12,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; -import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/tools/debug/log_file_viewer.dart'; diff --git a/pubspec.lock b/pubspec.lock index 7bc123180..32aeb5e61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -320,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0+3" + dropdown_button2: + dependency: "direct main" + description: + name: dropdown_button2 + sha256: "4458d81bfd24207f3d58f66f78097064e02f810f94cf1bc80bf20fe7685ebc80" + url: "https://pub.dev" + source: hosted + version: "2.0.0" email_validator: dependency: "direct main" description: @@ -904,10 +912,42 @@ packages: dependency: "direct main" description: name: local_auth - sha256: d3fece0749101725b03206f84a7dab7aaafb702dbbd09131ff8d8173259a9b19 + sha256: "0cf238be2bfa51a6c9e7e9cfc11c05ea39f2a3a4d3e5bb255d0ebc917da24401" url: "https://pub.dev" source: hosted - version: "1.1.11" + version: "2.1.6" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "2ccfadbb6fbc63e6674ad58a350b06188829e62669d67a0c752c4e43cb88272a" + url: "https://pub.dev" + source: hosted + version: "1.0.21" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + sha256: "604078f6492fe7730fc5bb8e4f2cfe2bc287a9b499ea0ff30a29925fc1873728" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "9e160d59ef0743e35f1b50f4fb84dc64f55676b1b8071e319ef35e7f3bc13367" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bfe0deede77fb36faa62799977074f35ac096d7cafce0c29a44a173d2a2a4b94 + url: "https://pub.dev" + source: hosted + version: "1.0.7" logging: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c52b3682a..16b2a754e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.31+431 +version: 0.7.37+437 environment: sdk: '>=2.17.0 <3.0.0' @@ -30,13 +30,14 @@ dependencies: collection: # dart computer: ^2.0.0 confetti: ^0.6.0 - crypto: ^3.0.2 connectivity_plus: ^3.0.3 + crypto: ^3.0.2 cupertino_icons: ^1.0.0 device_info: ^2.0.2 dio: ^4.0.6 dots_indicator: ^2.0.0 dotted_border: ^2.0.0+2 + dropdown_button2: ^2.0.0 email_validator: ^2.0.1 equatable: ^2.0.5 event_bus: ^2.0.0 @@ -75,7 +76,7 @@ dependencies: intl: ^0.17.0 like_button: ^2.0.2 loading_animations: ^2.1.0 - local_auth: ^1.1.5 + local_auth: ^2.1.5 logging: ^1.0.1 lottie: ^1.2.2 media_extension: ^1.0.1 @@ -165,7 +166,9 @@ flutter_native_splash: flutter: assets: - assets/ - - assets/models/ + - assets/models/cocossd/ + - assets/models/mobilenet/ + - assets/models/scenes/ fonts: - family: Inter fonts: