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