Merge remote-tracking branch 'refs/remotes/origin/homewidget' into homewidget
This commit is contained in:
commit
c5ce98971f
70 changed files with 2924 additions and 459 deletions
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -10,8 +10,8 @@ jobs:
|
|||
steps:
|
||||
|
||||
# Setup Java environment in order to build the Android app.
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
|
@ -47,18 +47,18 @@ jobs:
|
|||
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
# Upload generated apk to the artifacts.
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apk
|
||||
path: build/app/outputs/flutter-apk/ente.apk
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-checksum
|
||||
path: build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
# Create a pre-release
|
||||
- uses: ncipollo/release-action@v1
|
||||
- uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
artifacts: "build/app/outputs/flutter-apk/ente.apk,build/app/outputs/flutter-apk/sha256sum"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
3
android/.gitignore
vendored
3
android/.gitignore
vendored
|
@ -5,6 +5,3 @@ gradle-wrapper.jar
|
|||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Signing config files
|
||||
*.jks
|
|
@ -1,36 +1,36 @@
|
|||
ente ist eine einfache App, um Ihre Fotos und Videos automatisch zu sichern und zu organisieren.
|
||||
|
||||
Wenn Sie auf der Suche nach einer privaten Alternative sind, um Ihre Erinnerungen zu bewahren, sind Sie an der richtigen Stelle. Die müssen nicht mal ente haben. ente benötigt bestimmte Berechtigungen um als Anbieter eines Fotospeichers fungieren zu können. Mit Ente werden sie Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können.
|
||||
Wenn Sie auf der Suche nach einer datenschutzfreundlichen Alternative zu Google Fotos sind, sind Sie an der richtigen Stelle. Mit Ente werden Ihre Fotos Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können.
|
||||
|
||||
Wir haben Open-Source Apps auf allen Plattformen, und Ihre Fotos werden nahtlos zwischen all Ihren Geräten verschlüsselt (e2ee) synchronisiert.
|
||||
Ihre Fotos werden verschlüsselt (e2ee) zwischen allen Geräten synchronisiert.
|
||||
|
||||
ente ermöglicht es deine Alben simpel & schnell mit deinen Geliebten zu teilen. Du kannst öffentlich einsehbare Links teilen, wo sie dein Album sehen und zusammenarbeiten können, indem sie Fotos hinzufügen, sogar ohne einen Account oder eine App.
|
||||
ente ermöglicht es, deine Alben simpel & schnell mit deinen Geliebten zu teilen. Sie können öffentlich einsehbare Links teilen, sodass andere sogar ohne einen Account oder eine App Ihr Album sehen und darin zusammenarbeiten können, indem sie Fotos hinzufügen.
|
||||
|
||||
Ihre verschlüsselten Daten werden zu 3 verschiedenen Orten repliziert, unter anderem zu einem Schutzbunker in Paris. Wir nehmen die Erhaltung der Nachwelt ernst und machen es Dir leicht, dafür zu sorgen, dass Deine Erinnerungen Dich überdauern.
|
||||
Ihre verschlüsselten Daten werden an 3 verschiedenen Orten gespeichert, unter anderem in einem Schutzbunker in Paris. Wir nehmen die Erhaltung der Nachwelt ernst und machen es Ihnen leicht, dafür zu sorgen, dass Ihre Erinnerungen Sie überdauern.
|
||||
|
||||
Wir sind hier, um die sicherste Foto-App aller Zeiten zu entwickeln, begleite uns auf unserem Weg!
|
||||
|
||||
FEATURES
|
||||
- Sicherungen in Originalqualität, jeder Pixel ist wichtig
|
||||
- Familienpläne, damit Du den Speicher mit Deiner Familie teilen kannst
|
||||
- Kollaborative Alben, sodass du nach einer Reise Fotos zusammenstellen kannst
|
||||
- Geteilte Ordner für den Fall, dass Dein Partner Deine "Kamera" Klicks genießen soll
|
||||
- Links zu einem Album, welche mit einem Passwort geschützt werden können
|
||||
- Möglichkeit Speicherplatz freizugeben, indem bereits gesicherte Daten auf dem Gerät entfernt werden
|
||||
- Menschliche Unterstützung, denn Sie sind es wert
|
||||
- Sicherungen in Originalqualität, weil jeder Pixel zählt
|
||||
- Familien-Abos, damit Sie den Speicherplatz mit Ihrer Familie teilen können
|
||||
- Kollaborative Alben, sodass Sie nach einer Reise Fotos sammeln können
|
||||
- Geteilte Ordner für den Fall, dass Ihr Partner Ihre "Kamera" Klicks genießen soll
|
||||
- Album-Links, die mit einem Passwort geschützt werden können
|
||||
- Möglichkeit, Speicherplatz freizugeben, indem bereits gesicherte Daten auf dem Gerät entfernt werden
|
||||
- Menschlicher Support, denn Sie sind es wert
|
||||
- Beschreibungen, damit Sie Ihre Erinnerungen beschriften und leicht wiederfinden können
|
||||
- Integrierte Bildbearbeitung, um den letzten Schliff zu geben
|
||||
- Favorisiere, verstecke und erlebe deine Erinnerungen, denn sie sind kostbar
|
||||
- Importieren Sie mit einem Klick von Google, Apple, Ihrer Festplatte und mehr
|
||||
- Foto-Editor, um Ihren Fotos den Feinschliff zu verpassen
|
||||
- Favorisieren, verstecken und erleben Sie Ihre Erinnerungen, denn sie sind kostbar
|
||||
- Ein-Klick-Import von Google, Apple, Ihrer Festplatte und mehr
|
||||
- Dunkles Theme, weil Ihre Fotos darin gut aussehen
|
||||
- 2FA, 3FA, biometrische Authentifizierung
|
||||
- und noch VIELES mehr!
|
||||
|
||||
BERECHTIGUNGEN
|
||||
Diese können unter folgendem Link betrachtet werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
|
||||
Diese können unter folgendem Link überprüft werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
|
||||
|
||||
PREIS
|
||||
Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden.
|
||||
|
||||
SUPPORT
|
||||
Wir sind stolz darauf einen persönlichen Support anzubieten. Falls Sie ein Abonnement besitzen, können Sie sich mit Ihrem Anliegen via E-Mail an team@ente.io wenden und erhalten eine Antwort innerhalb von 24 Stunden.
|
||||
Wir sind stolz darauf, einen persönlichen Support anzubieten. Falls Sie ein Abonnement besitzen, können Sie sich mit Ihrem Anliegen via E-Mail an team@ente.io wenden und erhalten eine Antwort innerhalb von 24 Stunden.
|
||||
|
|
36
fastlane/metadata/android/pt/full_description.txt
Normal file
36
fastlane/metadata/android/pt/full_description.txt
Normal file
|
@ -0,0 +1,36 @@
|
|||
ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
|
||||
|
||||
Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
|
||||
|
||||
Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).
|
||||
|
||||
ente também torna simples compartilhar seus álbuns com seus entes queridos, mesmo que eles não estejam no ente. Você pode compartilhar links para visualização pública, onde eles podem visualizar seu álbum e colaborar adicionando fotos a ele, mesmo sem uma conta ou app.
|
||||
|
||||
Seus dados criptografados são replicados em 3 locais diferentes, incluindo um abrigo avançado em Paris. Levamos a sério a nossa postura e fazemos com que seja fácil garantir que suas memórias vivam.
|
||||
|
||||
Estamos aqui para se tornar o app de fotos mais seguro de todos, venha entrar em nossa jornada!
|
||||
|
||||
RECURSOS
|
||||
- Cópia de qualidade original, porque cada pixel é importante
|
||||
- Planos de família, para que você possa compartilhar o armazenamento com sua família
|
||||
- Álbuns colaborativos, para que você possa agrupar fotos após uma corrida
|
||||
- Pastas compartilhadas, caso você queira que seu parceiro aproveite seus cliques da "Câmera"
|
||||
- Links de álbuns, que podem ser protegidos com uma senha e definidos para expirar
|
||||
- Capacidade de liberar espaço, removendo arquivos que foram salvos com segurança
|
||||
- Suporte humano, porque você vale a pena
|
||||
- Descrições, para que você possa captar suas memórias e encontrá-las facilmente
|
||||
- Editor de imagens, para adicionar toques finais
|
||||
- Favoritar, esconder e reviver suas memórias, pois elas são preciosas
|
||||
- Importar com um clique do Google, Apple, seu disco rígido e muito mais
|
||||
- Tema escuro, porque suas fotos parecem bem nele
|
||||
- 2FA, 3FA, Autenticação biométrica
|
||||
- e MUITO MAIS!
|
||||
|
||||
PERMISSÕES
|
||||
ente solicita certas permissões para servir o propósito de um provedor de armazenamento de fotos, que pode ser revisado aqui: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
|
||||
|
||||
PREÇO
|
||||
Não oferecemos planos gratuitos para sempre, porque é importante para nós que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io.
|
||||
|
||||
SUPORTE
|
||||
Temos orgulho em oferecer apoio humano. Temos orgulho em oferecer apoio humano. Se você é o nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas.
|
1
fastlane/metadata/android/pt/short_description.txt
Normal file
1
fastlane/metadata/android/pt/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ente é um aplicativo de armazenamento de fotos criptografado de ponta a ponta
|
1
fastlane/metadata/android/pt/title.txt
Normal file
1
fastlane/metadata/android/pt/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ente - armazenamento criptografado de fotos
|
33
fastlane/metadata/ios/pt/description.txt
Normal file
33
fastlane/metadata/ios/pt/description.txt
Normal file
|
@ -0,0 +1,33 @@
|
|||
Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
|
||||
|
||||
Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
|
||||
|
||||
Temos aplicativos de código aberto em Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).
|
||||
|
||||
Ente também torna simples compartilhar seus álbuns com seus entes queridos. Você pode compartilhá-los diretamente com outros usuários do Ente, criptografados de ponta a ponta; ou com links publicamente visíveis.
|
||||
|
||||
Seus dados criptografados são replicados em locais diferentes, incluindo um abrigo avançado em Paris. Levamos a sério a nossa postura e fazemos com que seja fácil garantir que suas memórias vivam.
|
||||
|
||||
Estamos aqui para se tornar o app de fotos mais seguro de todos, venha entrar em nossa jornada!
|
||||
|
||||
RECURSOS
|
||||
- Cópia de qualidade original, porque cada pixel é importante
|
||||
- Planos de família, para que você possa compartilhar o armazenamento com sua família
|
||||
- Pastas compartilhadas, caso você queira que seu parceiro aproveite seus cliques da "Câmera"
|
||||
- Links de álbuns, que podem ser protegidos com uma senha e definidos para expirar
|
||||
- Capacidade de liberar espaço, removendo arquivos que foram salvos com segurança
|
||||
- Editor de imagens, para adicionar toques finais
|
||||
- Favoritar, esconder e reviver suas memórias, pois elas são preciosas
|
||||
- Importar com um clique de todos os principais provedores de armazenamento
|
||||
- Tema escuro, porque suas fotos parecem bem nele
|
||||
- 2FA, 3FA, Autenticação biométrica
|
||||
- e MUITO MAIS!
|
||||
|
||||
PREÇO
|
||||
Não oferecemos planos gratuitos para sempre, porque é importante para nós que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io.
|
||||
|
||||
SUPORTE
|
||||
Temos orgulho em oferecer apoio humano. Temos orgulho em oferecer apoio humano. Se você é o nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas.
|
||||
|
||||
TERMOS
|
||||
https://ente.io/terms
|
1
fastlane/metadata/ios/pt/keywords.txt
Normal file
1
fastlane/metadata/ios/pt/keywords.txt
Normal file
|
@ -0,0 +1 @@
|
|||
fotos,fotografia,família,privacidade,nuvem,backup,vídeos,foto,criptografia,armazenamento,álbum,alternativa
|
1
fastlane/metadata/ios/pt/name.txt
Normal file
1
fastlane/metadata/ios/pt/name.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ente Fotos
|
1
fastlane/metadata/ios/pt/subtitle.txt
Normal file
1
fastlane/metadata/ios/pt/subtitle.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Armazenamento de fotos criptografado
|
|
@ -2,7 +2,7 @@ Ente ist eine einfache Anwendung zur automatischen Sicherung und Organisation Ih
|
|||
|
||||
Wenn Sie auf der Suche nach einer datenschutzfreundlichen Alternative sind, um Ihre Erinnerungen zu bewahren, sind Sie hier genau richtig. Mit Ente werden sie Ende-zu-Ende-verschlüsselt (e2ee) gespeichert. Das bedeutet, dass nur Sie sie sehen können.
|
||||
|
||||
Wir haben Apps für Android, iOS, Web und Desktop und Ihre Fotos werden nahtlos zwischen all Ihren Geräten auf eine Ende-zu-Ende-verschlüsselte (e2ee) Weise synchronisiert.
|
||||
Wir haben Open-Source-Apps für Android, iOS, Web und Desktop. Ihre Fotos werden verschlüsselt (e2ee) zwischen allen Geräten synchronisiert.
|
||||
|
||||
Mit Ente ist es auch ganz einfach, Ihre Alben mit Ihren Lieben zu teilen. Sie können sie entweder direkt mit anderen Ente-Benutzern teilen, Ende-zu-Ende-verschlüsselt, oder mit öffentlich einsehbaren Links.
|
||||
|
||||
|
@ -12,11 +12,11 @@ Wir sind hier, um die sicherste Foto-App aller Zeiten zu entwickeln, begleiten S
|
|||
|
||||
✨ FUNKTIONEN
|
||||
- Sicherungen in Originalqualität, denn jeder Pixel ist wichtig
|
||||
- Familien-Abos, damit Sie den Speicherplatz mit Ihrer Familie teilen können
|
||||
- Familien-Abos, damit Sie den Speicherplatz mit Ihrer Familie teilen können
|
||||
- Gemeinsame Ordner, falls Sie möchten, dass Ihr Partner Ihre „Kamera“-Klicks genießen kann
|
||||
- Album-Links, die mit einem Passwort geschützt werden können und deren Gültigkeit abläuft
|
||||
- Möglichkeit, Speicherplatz freizugeben, indem Dateien, die sicher gespeichert wurden, entfernt werden
|
||||
- Bildbearbeitungsprogramm, um den letzten Schliff zu geben
|
||||
- Foto-Editor, um Ihren Fotos den Feinschliff zu verpassen
|
||||
- Favorisieren, verstecken und erleben Sie Ihre Erinnerungen, denn sie sind kostbar
|
||||
- Ein-Klick-Import von Google, Apple, Ihrer Festplatte und mehr
|
||||
- Dunkles Theme, weil Ihre Fotos darin gut aussehen
|
||||
|
|
30
fastlane/metadata/playstore/pt/full_description.txt
Normal file
30
fastlane/metadata/playstore/pt/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
|
||||
|
||||
Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografados de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
|
||||
|
||||
Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).
|
||||
|
||||
Ente também torna simples compartilhar seus álbuns com seus entes queridos. Você pode compartilhá-los diretamente com outros usuários do Ente, criptografados de ponta a ponta; ou com links publicamente visíveis.
|
||||
|
||||
Seus dados criptografados são replicados em locais diferentes, incluindo um abrigo avançado em Paris. Levamos a sério a nossa postura e fazemos com que seja fácil garantir que suas memórias vivam.
|
||||
|
||||
Estamos aqui para se tornar o app de fotos mais seguro de todos, venha entrar em nossa jornada!
|
||||
|
||||
✨ RECURSOS
|
||||
- Cópia de qualidade original, porque cada pixel é importante
|
||||
- Planos de família, para que você possa compartilhar o armazenamento com sua família
|
||||
- Pastas compartilhadas, caso você queira que seu parceiro aproveite seus cliques da "Câmera"
|
||||
- Links de álbuns, que podem ser protegidos com uma senha e definidos para expirar
|
||||
- Capacidade de liberar espaço, removendo arquivos que foram salvos com segurança
|
||||
- Editor de imagens, para adicionar toques finais
|
||||
- Favoritar, esconder e reviver suas memórias, pois elas são preciosas
|
||||
- Importar com um clique do Google, Apple, seu disco rígido e muito mais
|
||||
- Tema escuro, porque suas fotos parecem bem nele
|
||||
- 2FA, 3FA, Autenticação biométrica
|
||||
- e MUITO MAIS!
|
||||
|
||||
💲 PREÇO
|
||||
Não oferecemos planos gratuitos para sempre, porque é importante para nós que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io.
|
||||
|
||||
🙋 SUPORTE
|
||||
Temos orgulho em oferecer apoio humano. Temos orgulho em oferecer apoio humano. Se você é o nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas.
|
1
fastlane/metadata/playstore/pt/short_description.txt
Normal file
1
fastlane/metadata/playstore/pt/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Armazenamento de fotos criptografado - backup, organize e compartilhe suas fotos e vídeos
|
1
fastlane/metadata/playstore/pt/title.txt
Normal file
1
fastlane/metadata/playstore/pt/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ente Fotos
|
|
@ -1,6 +1,8 @@
|
|||
PODS:
|
||||
- background_fetch (1.2.2):
|
||||
- Flutter
|
||||
- battery_info (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
|
@ -215,6 +217,7 @@ PODS:
|
|||
|
||||
DEPENDENCIES:
|
||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
|
@ -289,6 +292,8 @@ SPEC REPOS:
|
|||
EXTERNAL SOURCES:
|
||||
background_fetch:
|
||||
:path: ".symlinks/plugins/background_fetch/ios"
|
||||
battery_info:
|
||||
:path: ".symlinks/plugins/battery_info/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
|
@ -383,7 +388,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_fetch: ec64adecd504f2d0d333b4b11d31f47c8ee23d12
|
||||
background_fetch: 896944864b038d2837fc750d470e9841e1e6a363
|
||||
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||
connectivity_plus: 53efb943fc2882c8512d84c45707bcabc4c36076
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
|
|
|
@ -367,6 +367,7 @@
|
|||
"${BUILT_PRODUCTS_DIR}/SentryPrivate/SentryPrivate.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||
|
@ -449,6 +450,7 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SentryPrivate.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||
|
|
28
lib/app.dart
28
lib/app.dart
|
@ -13,7 +13,7 @@ import 'package:photos/ente_theme_data.dart';
|
|||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/tabs/home_widget.dart';
|
||||
import "package:photos/ui/viewer/actions/file_viewer.dart";
|
||||
|
@ -43,12 +43,8 @@ class EnteApp extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
||||
static const initialInteractionTimeout = Duration(seconds: 10);
|
||||
static const defaultInteractionTimeout = Duration(seconds: 5);
|
||||
|
||||
final _logger = Logger("EnteAppState");
|
||||
late Locale locale;
|
||||
late Timer _userInteractionTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -57,7 +53,6 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
locale = widget.locale;
|
||||
setupIntentAction();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_setupInteractionTimer(timeout: initialInteractionTimeout);
|
||||
}
|
||||
|
||||
setLocale(Locale newLocale) {
|
||||
|
@ -76,30 +71,12 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
void _resetTimer() {
|
||||
_userInteractionTimer.cancel();
|
||||
_setupInteractionTimer();
|
||||
}
|
||||
|
||||
void _setupInteractionTimer({Duration timeout = defaultInteractionTimeout}) {
|
||||
if (Platform.isAndroid || kDebugMode) {
|
||||
_userInteractionTimer = Timer(timeout, () {
|
||||
debugPrint("user is not interacting with the app");
|
||||
SemanticSearchService.instance.resumeIndexing();
|
||||
});
|
||||
} else {
|
||||
SemanticSearchService.instance.resumeIndexing();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Platform.isAndroid || kDebugMode) {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
SemanticSearchService.instance.pauseIndexing();
|
||||
debugPrint("user is interacting with the app");
|
||||
_resetTimer();
|
||||
MachineLearningController.instance.onUserInteraction();
|
||||
},
|
||||
child: AdaptiveTheme(
|
||||
light: lightThemeData,
|
||||
|
@ -149,7 +126,6 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_userInteractionTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,9 +25,9 @@ import 'package:photos/services/billing_service.dart';
|
|||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import 'package:photos/services/ignored_files_service.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
|
|
|
@ -67,6 +67,32 @@ const galleryGridSpacing = 2.0;
|
|||
|
||||
const searchSectionLimit = 7;
|
||||
|
||||
bool isInternalUser = false;
|
||||
|
||||
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
|
||||
|
||||
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
|
||||
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +
|
||||
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC' +
|
||||
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF' +
|
||||
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
|
||||
'6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL' +
|
||||
'W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA' +
|
||||
'AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY' +
|
||||
'nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK' +
|
||||
'kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD' +
|
||||
'AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC' +
|
||||
'gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
|
||||
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK' +
|
||||
'ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
|
||||
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=';
|
||||
|
|
|
@ -48,6 +48,19 @@ class EmbeddingsDB {
|
|||
return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll();
|
||||
}
|
||||
|
||||
Future<void> deleteEmbeddings(List<int> fileIDs) async {
|
||||
await _isar.writeTxn(() async {
|
||||
final embeddings = <Embedding>[];
|
||||
for (final fileID in fileIDs) {
|
||||
embeddings.addAll(
|
||||
await _isar.embeddings.filter().fileIDEqualTo(fileID).findAll(),
|
||||
);
|
||||
}
|
||||
await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList());
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAllForModel(Model model) async {
|
||||
await _isar.writeTxn(() async {
|
||||
final embeddings =
|
||||
|
|
7
lib/events/machine_learning_control_event.dart
Normal file
7
lib/events/machine_learning_control_event.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import "package:photos/events/event.dart";
|
||||
|
||||
class MachineLearningControlEvent extends Event {
|
||||
final bool shouldRun;
|
||||
|
||||
MachineLearningControlEvent(this.shouldRun);
|
||||
}
|
1199
lib/generated/intl/messages_pt.dart
generated
1199
lib/generated/intl/messages_pt.dart
generated
File diff suppressed because it is too large
Load diff
2
lib/generated/intl/messages_zh.dart
generated
2
lib/generated/intl/messages_zh.dart
generated
|
@ -666,7 +666,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"),
|
||||
"itemsWillBeRemovedFromAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("所选项目将从此相册中移除"),
|
||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("加入 Discord"),
|
||||
"keepPhotos": MessageLookupByLibrary.simpleMessage("保留照片"),
|
||||
"kiloMeterUnit": MessageLookupByLibrary.simpleMessage("公里"),
|
||||
"kindlyHelpUsWithThisInformation":
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -33,11 +33,12 @@ import 'package:photos/services/feature_flag_service.dart';
|
|||
import 'package:photos/services/local_file_update_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import 'package:photos/services/push_service.dart';
|
||||
import 'package:photos/services/remote_sync_service.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import 'package:photos/services/semantic_search/semantic_search_service.dart';
|
||||
import "package:photos/services/storage_bonus_service.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/services/trash_sync_service.dart';
|
||||
|
@ -312,6 +313,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
}
|
||||
unawaited(FeatureFlagService.instance.init());
|
||||
unawaited(SemanticSearchService.instance.init());
|
||||
MachineLearningController.instance.init();
|
||||
// Can not including existing tf/ml binaries as they are not being built
|
||||
// from source.
|
||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
||||
|
|
|
@ -11,6 +11,7 @@ const heightKey = 'h';
|
|||
const latKey = "lat";
|
||||
const longKey = "long";
|
||||
const motionVideoIndexKey = "mvi";
|
||||
const noThumbKey = "noThumb";
|
||||
|
||||
class MagicMetadata {
|
||||
// 0 -> visible
|
||||
|
@ -47,6 +48,13 @@ class PubMagicMetadata {
|
|||
// photo
|
||||
int? mvi;
|
||||
|
||||
// if true, then the thumbnail is not available
|
||||
// Note: desktop/web sets hasStaticThumbnail in the file metadata.
|
||||
// As we don't want to support updating the og file metadata (yet), adding
|
||||
// this new field to the pub metadata. For static thumbnail, all thumbnails
|
||||
// should have exact same hash with should match the constant `blackThumbnailBase64`
|
||||
bool? noThumb;
|
||||
|
||||
PubMagicMetadata({
|
||||
this.editedTime,
|
||||
this.editedName,
|
||||
|
@ -57,6 +65,7 @@ class PubMagicMetadata {
|
|||
this.lat,
|
||||
this.long,
|
||||
this.mvi,
|
||||
this.noThumb,
|
||||
});
|
||||
|
||||
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
|
@ -77,6 +86,7 @@ class PubMagicMetadata {
|
|||
lat: map[latKey],
|
||||
long: map[longKey],
|
||||
mvi: map[motionVideoIndexKey],
|
||||
noThumb: map[noThumbKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import "package:photos/events/location_tag_updated_event.dart";
|
|||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/collection/collection.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import "package:photos/models/search/generic_search_result.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/typedefs.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
|
@ -24,6 +25,7 @@ enum ResultType {
|
|||
collection,
|
||||
file,
|
||||
location,
|
||||
locationSuggestion,
|
||||
month,
|
||||
year,
|
||||
fileType,
|
||||
|
@ -243,10 +245,10 @@ extension SectionTypeExtensions on SectionType {
|
|||
}) {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
return Future.value(List<GenericSearchResult>.empty());
|
||||
|
||||
case SectionType.content:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
return Future.value(List<GenericSearchResult>.empty());
|
||||
|
||||
case SectionType.moment:
|
||||
return SearchService.instance.getRandomMomentsSearchResults(context);
|
||||
|
|
|
@ -71,10 +71,9 @@ class FeatureFlagService {
|
|||
bool isInternalUserOrDebugBuild() {
|
||||
final String? email = Configuration.instance.getEmail();
|
||||
final userID = Configuration.instance.getUserID();
|
||||
isInternalUser = (email != null && email.endsWith("@ente.io")) ||
|
||||
return (email != null && email.endsWith("@ente.io")) ||
|
||||
_internalUserIDs.contains(userID) ||
|
||||
kDebugMode;
|
||||
return isInternalUser;
|
||||
}
|
||||
|
||||
Future<void> fetchFeatureFlags() async {
|
||||
|
|
|
@ -7,6 +7,7 @@ import "package:logging/logging.dart";
|
|||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/extensions/stop_watch.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
|
@ -45,6 +46,8 @@ class LocationService {
|
|||
List<EnteFile> allFiles,
|
||||
String query,
|
||||
) async {
|
||||
final EnteWatch w = EnteWatch("cities_search")..start();
|
||||
w.log('start for files ${allFiles.length} and query $query');
|
||||
final result = await _computer.compute(
|
||||
getCityResults,
|
||||
param: {
|
||||
|
@ -53,6 +56,10 @@ class LocationService {
|
|||
"files": allFiles,
|
||||
},
|
||||
);
|
||||
w.log(
|
||||
'end for query: $query on ${allFiles.length} files, found '
|
||||
'${result.length} cities',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -235,31 +242,29 @@ Future<List<City>> parseCities(Map args) async {
|
|||
|
||||
Map<City, List<EnteFile>> getCityResults(Map args) {
|
||||
final query = (args["query"] as String).toLowerCase();
|
||||
final cities = args["cities"] as List<City>;
|
||||
final files = args["files"] as List<EnteFile>;
|
||||
final List<City> cities = args["cities"] as List<City>;
|
||||
final List<EnteFile> files = args["files"] as List<EnteFile>;
|
||||
|
||||
final matchingCities = cities.where(
|
||||
(city) => city.city.toLowerCase().contains(query),
|
||||
);
|
||||
final matchingCities = cities
|
||||
.where(
|
||||
(city) => city.city.toLowerCase().contains(query),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final Map<City, List<EnteFile>> results = {};
|
||||
for (final city in matchingCities) {
|
||||
final List<EnteFile> matchingFiles = [];
|
||||
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
||||
for (final file in files) {
|
||||
if (file.hasLocation) {
|
||||
if (isFileInsideLocationTag(
|
||||
cityLocation,
|
||||
file.location!,
|
||||
defaultCityRadius,
|
||||
)) {
|
||||
matchingFiles.add(file);
|
||||
}
|
||||
for (final file in files) {
|
||||
if (!file.hasLocation) continue; // Skip files without location
|
||||
for (final city in matchingCities) {
|
||||
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
||||
if (isFileInsideLocationTag(
|
||||
cityLocation,
|
||||
file.location!,
|
||||
defaultCityRadius,
|
||||
)) {
|
||||
results.putIfAbsent(city, () => []).add(file);
|
||||
break; // Stop searching once a file is matched with a city
|
||||
}
|
||||
}
|
||||
if (matchingFiles.isNotEmpty) {
|
||||
results[city] = matchingFiles;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import "package:battery_info/battery_info_plugin.dart";
|
||||
import "package:battery_info/model/android_battery_info.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/machine_learning_control_event.dart";
|
||||
|
||||
class MachineLearningController {
|
||||
MachineLearningController._privateConstructor();
|
||||
|
||||
static final MachineLearningController instance =
|
||||
MachineLearningController._privateConstructor();
|
||||
|
||||
final _logger = Logger("MachineLearningController");
|
||||
|
||||
static const kMaximumTemperature = 42; // 42 degree celsius
|
||||
static const kMinimumBatteryLevel = 20; // 20%
|
||||
static const kInitialInteractionTimeout = Duration(seconds: 10);
|
||||
static const kDefaultInteractionTimeout = Duration(seconds: 5);
|
||||
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
|
||||
|
||||
bool _isDeviceHealthy = true;
|
||||
bool _isUserInteracting = true;
|
||||
bool _isRunningML = false;
|
||||
late Timer _userInteractionTimer;
|
||||
|
||||
void init() {
|
||||
if (Platform.isAndroid) {
|
||||
_startInteractionTimer(timeout: kInitialInteractionTimeout);
|
||||
BatteryInfoPlugin()
|
||||
.androidBatteryInfoStream
|
||||
.listen((AndroidBatteryInfo? batteryInfo) {
|
||||
_onBatteryStateUpdate(batteryInfo);
|
||||
});
|
||||
} else {
|
||||
// Always run Machine Learning on iOS
|
||||
Bus.instance.fire(MachineLearningControlEvent(true));
|
||||
}
|
||||
}
|
||||
|
||||
void onUserInteraction() {
|
||||
_logger.info("User is interacting with the app");
|
||||
_isUserInteracting = true;
|
||||
_fireControlEvent();
|
||||
_resetTimer();
|
||||
}
|
||||
|
||||
void _fireControlEvent() {
|
||||
final shouldRunML = _isDeviceHealthy && !_isUserInteracting;
|
||||
if (shouldRunML != _isRunningML) {
|
||||
_isRunningML = shouldRunML;
|
||||
_logger.info(
|
||||
"Firing event with device health: $_isDeviceHealthy and user interaction: $_isUserInteracting",
|
||||
);
|
||||
Bus.instance.fire(MachineLearningControlEvent(shouldRunML));
|
||||
}
|
||||
}
|
||||
|
||||
void _startInteractionTimer({Duration timeout = kDefaultInteractionTimeout}) {
|
||||
_userInteractionTimer = Timer(timeout, () {
|
||||
_logger.info("User is not interacting with the app");
|
||||
_isUserInteracting = false;
|
||||
_fireControlEvent();
|
||||
});
|
||||
}
|
||||
|
||||
void _resetTimer() {
|
||||
_userInteractionTimer.cancel();
|
||||
_startInteractionTimer();
|
||||
}
|
||||
|
||||
void _onBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) {
|
||||
_logger.info("Battery info: ${batteryInfo!.toJson()}");
|
||||
_isDeviceHealthy = _computeIsDeviceHealthy(batteryInfo);
|
||||
_fireControlEvent();
|
||||
}
|
||||
|
||||
bool _computeIsDeviceHealthy(AndroidBatteryInfo info) {
|
||||
return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel) &&
|
||||
_isAcceptableTemperature(info.temperature ?? kMaximumTemperature) &&
|
||||
_isBatteryHealthy(info.health ?? "");
|
||||
}
|
||||
|
||||
bool _hasSufficientBattery(int batteryLevel) {
|
||||
return batteryLevel >= kMinimumBatteryLevel;
|
||||
}
|
||||
|
||||
bool _isAcceptableTemperature(int temperature) {
|
||||
return temperature <= kMaximumTemperature;
|
||||
}
|
||||
|
||||
bool _isBatteryHealthy(String health) {
|
||||
return !kUnhealthyStates.contains(health);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import "package:photos/db/embeddings_db.dart";
|
|||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/embedding.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/semantic_search/remote_embedding.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/remote_embedding.dart';
|
||||
import "package:photos/utils/crypto_util.dart";
|
||||
import "package:photos/utils/file_download_util.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
@ -53,13 +53,22 @@ class EmbeddingStore {
|
|||
final fileMap = await FilesDB.instance
|
||||
.getFilesFromIDs(pendingItems.map((e) => e.fileID).toList());
|
||||
_logger.info("Pushing ${pendingItems.length} embeddings");
|
||||
final deletedEntries = <int>[];
|
||||
for (final item in pendingItems) {
|
||||
try {
|
||||
await _pushEmbedding(fileMap[item.fileID]!, item);
|
||||
final file = fileMap[item.fileID];
|
||||
if (file != null) {
|
||||
await _pushEmbedding(file, item);
|
||||
} else {
|
||||
deletedEntries.add(item.fileID);
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
}
|
||||
}
|
||||
if (deletedEntries.isNotEmpty) {
|
||||
await EmbeddingsDB.instance.deleteEmbeddings(deletedEntries);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> storeEmbedding(EnteFile file, Embedding embedding) async {
|
|
@ -1,7 +1,7 @@
|
|||
import "package:clip_ggml/clip_ggml.dart";
|
||||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/services/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
|
||||
class GGML extends MLFramework {
|
||||
static const kModelBucketEndpoint = "https://models.ente.io/";
|
|
@ -2,15 +2,13 @@ import "dart:async";
|
|||
import "dart:io";
|
||||
|
||||
import "package:connectivity_plus/connectivity_plus.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:path/path.dart";
|
||||
import "package:path_provider/path_provider.dart";
|
||||
import "package:photos/core/errors.dart";
|
||||
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/services/remote_assets_service.dart";
|
||||
|
||||
abstract class MLFramework {
|
||||
static const kImageEncoderEnabled = true;
|
||||
|
@ -105,46 +103,20 @@ abstract class MLFramework {
|
|||
return;
|
||||
}
|
||||
_initState = InitializationState.initializingImageModel;
|
||||
final path = await _getLocalImageModelPath();
|
||||
if (await File(path).exists()) {
|
||||
await loadImageModel(path);
|
||||
} else {
|
||||
_initState = InitializationState.downloadingImageModel;
|
||||
final tempFile = File(path + ".temp");
|
||||
await _downloadFile(getImageModelRemotePath(), tempFile.path);
|
||||
await tempFile.rename(path);
|
||||
await loadImageModel(path);
|
||||
}
|
||||
final imageModel =
|
||||
await RemoteAssetsService.instance.getAsset(getImageModelRemotePath());
|
||||
await loadImageModel(imageModel.path);
|
||||
_initState = InitializationState.initializedImageModel;
|
||||
}
|
||||
|
||||
Future<void> _initTextModel() async {
|
||||
final path = await _getLocalTextModelPath();
|
||||
_initState = InitializationState.initializingTextModel;
|
||||
if (await File(path).exists()) {
|
||||
await loadTextModel(path);
|
||||
} else {
|
||||
_initState = InitializationState.downloadingTextModel;
|
||||
final tempFile = File(path + ".temp");
|
||||
await _downloadFile(getTextModelRemotePath(), tempFile.path);
|
||||
await tempFile.rename(path);
|
||||
await loadTextModel(path);
|
||||
}
|
||||
final textModel =
|
||||
await RemoteAssetsService.instance.getAsset(getTextModelRemotePath());
|
||||
await loadTextModel(textModel.path);
|
||||
_initState = InitializationState.initializedTextModel;
|
||||
}
|
||||
|
||||
Future<String> _getLocalImageModelPath() async {
|
||||
return (await getTemporaryDirectory()).path +
|
||||
"/models/" +
|
||||
basename(getImageModelRemotePath());
|
||||
}
|
||||
|
||||
Future<String> _getLocalTextModelPath() async {
|
||||
return (await getTemporaryDirectory()).path +
|
||||
"/models/" +
|
||||
basename(getTextModelRemotePath());
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(
|
||||
String url,
|
||||
String savePath, {
|
||||
|
@ -176,17 +148,6 @@ abstract class MLFramework {
|
|||
return connectivityResult != ConnectivityResult.mobile ||
|
||||
shouldDownloadOverMobileData;
|
||||
}
|
||||
|
||||
Future<String> getAccessiblePathForAsset(
|
||||
String assetPath,
|
||||
String tempName,
|
||||
) async {
|
||||
final byteData = await rootBundle.load(assetPath);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = await File('${tempDir.path}/$tempName')
|
||||
.writeAsBytes(byteData.buffer.asUint8List());
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
class MLFrameworkInitializationUpdateEvent extends Event {
|
||||
|
@ -198,10 +159,8 @@ class MLFrameworkInitializationUpdateEvent extends Event {
|
|||
enum InitializationState {
|
||||
notInitialized,
|
||||
waitingForNetwork,
|
||||
downloadingImageModel,
|
||||
initializingImageModel,
|
||||
initializedImageModel,
|
||||
downloadingTextModel,
|
||||
initializingTextModel,
|
||||
initializedTextModel,
|
||||
initialized,
|
|
@ -1,9 +1,9 @@
|
|||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:onnxruntime/onnxruntime.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/onnx/onnx_image_encoder.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/onnx/onnx_text_encoder.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_text_encoder.dart';
|
||||
|
||||
class ONNX extends MLFramework {
|
||||
static const kModelBucketEndpoint = "https://models.ente.io/";
|
|
@ -5,7 +5,7 @@ import "dart:typed_data";
|
|||
import "package:flutter/services.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:onnxruntime/onnxruntime.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/onnx/onnx_text_tokenizer.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_text_tokenizer.dart';
|
||||
|
||||
class OnnxTextEncoder {
|
||||
static const kVocabFilePath = "assets/models/clip/bpe_simple_vocab_16e6.txt";
|
|
@ -1,5 +1,6 @@
|
|||
import "dart:async";
|
||||
import "dart:collection";
|
||||
import "dart:io";
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
|
@ -11,14 +12,16 @@ import "package:photos/db/files_db.dart";
|
|||
import "package:photos/events/diff_sync_complete_event.dart";
|
||||
import 'package:photos/events/embedding_updated_event.dart';
|
||||
import "package:photos/events/file_uploaded_event.dart";
|
||||
import "package:photos/events/machine_learning_control_event.dart";
|
||||
import "package:photos/models/embedding.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/semantic_search/embedding_store.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ggml.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
||||
import 'package:photos/services/semantic_search/frameworks/onnx/onnx.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/embedding_store.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ggml.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx.dart';
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/device_info.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
import "package:photos/utils/thumbnail_util.dart";
|
||||
|
||||
|
@ -33,7 +36,6 @@ class SemanticSearchService {
|
|||
static const kEmbeddingLength = 512;
|
||||
static const kScoreThreshold = 0.23;
|
||||
static const kShouldPushEmbeddings = true;
|
||||
static const kCurrentModel = Model.onnxClip;
|
||||
static const kDebounceDuration = Duration(milliseconds: 4000);
|
||||
|
||||
final _logger = Logger("SemanticSearchService");
|
||||
|
@ -42,6 +44,7 @@ class SemanticSearchService {
|
|||
final _embeddingLoaderDebouncer =
|
||||
Debouncer(kDebounceDuration, executionInterval: kDebounceDuration);
|
||||
|
||||
late Model _currentModel;
|
||||
late MLFramework _mlFramework;
|
||||
bool _hasInitialized = false;
|
||||
bool _isComputingEmbeddings = false;
|
||||
|
@ -49,22 +52,10 @@ class SemanticSearchService {
|
|||
Future<List<EnteFile>>? _ongoingRequest;
|
||||
List<Embedding> _cachedEmbeddings = <Embedding>[];
|
||||
PendingQuery? _nextQuery;
|
||||
Completer<void> _userInteraction = Completer<void>();
|
||||
Completer<void> _mlController = Completer<void>();
|
||||
|
||||
get hasInitialized => _hasInitialized;
|
||||
|
||||
void resumeIndexing() {
|
||||
_logger.info("Resuming indexing");
|
||||
_userInteraction.complete();
|
||||
}
|
||||
|
||||
void pauseIndexing() {
|
||||
if (_userInteraction.isCompleted) {
|
||||
_logger.info("Pausing indexing");
|
||||
_userInteraction = Completer<void>();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> init({bool shouldSyncImmediately = false}) async {
|
||||
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
||||
return;
|
||||
|
@ -76,7 +67,8 @@ class SemanticSearchService {
|
|||
_hasInitialized = true;
|
||||
final shouldDownloadOverMobileData =
|
||||
Configuration.instance.shouldBackupOverMobileData();
|
||||
_mlFramework = kCurrentModel == Model.onnxClip
|
||||
_currentModel = await _getCurrentModel();
|
||||
_mlFramework = _currentModel == Model.onnxClip
|
||||
? ONNX(shouldDownloadOverMobileData)
|
||||
: GGML(shouldDownloadOverMobileData);
|
||||
await EmbeddingsDB.instance.init();
|
||||
|
@ -109,6 +101,17 @@ class SemanticSearchService {
|
|||
if (shouldSyncImmediately) {
|
||||
unawaited(sync());
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||
if (event.shouldRun) {
|
||||
_startIndexing();
|
||||
} else {
|
||||
_pauseIndexing();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_startIndexing();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> release() async {
|
||||
|
@ -122,7 +125,7 @@ class SemanticSearchService {
|
|||
return;
|
||||
}
|
||||
_isSyncing = true;
|
||||
await EmbeddingStore.instance.pullEmbeddings(kCurrentModel);
|
||||
await EmbeddingStore.instance.pullEmbeddings(_currentModel);
|
||||
await _backFill();
|
||||
_isSyncing = false;
|
||||
}
|
||||
|
@ -171,14 +174,14 @@ class SemanticSearchService {
|
|||
}
|
||||
|
||||
Future<void> clearIndexes() async {
|
||||
await EmbeddingStore.instance.clearEmbeddings(kCurrentModel);
|
||||
_logger.info("Indexes cleared for $kCurrentModel");
|
||||
await EmbeddingStore.instance.clearEmbeddings(_currentModel);
|
||||
_logger.info("Indexes cleared for $_currentModel");
|
||||
}
|
||||
|
||||
Future<void> _loadEmbeddings() async {
|
||||
_logger.info("Pulling cached embeddings");
|
||||
final startTime = DateTime.now();
|
||||
_cachedEmbeddings = await EmbeddingsDB.instance.getAll(kCurrentModel);
|
||||
_cachedEmbeddings = await EmbeddingsDB.instance.getAll(_currentModel);
|
||||
final endTime = DateTime.now();
|
||||
_logger.info(
|
||||
"Loading ${_cachedEmbeddings.length} took: ${(endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch)}ms",
|
||||
|
@ -240,15 +243,23 @@ class SemanticSearchService {
|
|||
|
||||
final ignoredCollections =
|
||||
CollectionsService.instance.getHiddenCollectionIds();
|
||||
final deletedEntries = <int>[];
|
||||
for (final result in queryResults) {
|
||||
final file = filesMap[result.id];
|
||||
if (file != null && !ignoredCollections.contains(file.collectionID)) {
|
||||
results.add(filesMap[result.id]!);
|
||||
results.add(file);
|
||||
}
|
||||
if (file == null) {
|
||||
deletedEntries.add(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.info(results.length.toString() + " results");
|
||||
|
||||
if (deletedEntries.isNotEmpty) {
|
||||
unawaited(EmbeddingsDB.instance.deleteEmbeddings(deletedEntries));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
@ -292,9 +303,9 @@ class SemanticSearchService {
|
|||
if (!_frameworkInitialization.isCompleted) {
|
||||
return;
|
||||
}
|
||||
if (!_userInteraction.isCompleted) {
|
||||
_logger.info("Waiting for user interactions to stop...");
|
||||
await _userInteraction.future;
|
||||
if (!_mlController.isCompleted) {
|
||||
_logger.info("Waiting for a green signal from controller...");
|
||||
await _mlController.future;
|
||||
}
|
||||
try {
|
||||
final thumbnail = await getThumbnailForUploadedFile(file);
|
||||
|
@ -312,7 +323,7 @@ class SemanticSearchService {
|
|||
|
||||
final embedding = Embedding(
|
||||
fileID: file.uploadedFileID!,
|
||||
model: kCurrentModel,
|
||||
model: _currentModel,
|
||||
embedding: result,
|
||||
);
|
||||
await EmbeddingStore.instance.storeEmbedding(
|
||||
|
@ -359,6 +370,28 @@ class SemanticSearchService {
|
|||
);
|
||||
return queryResults;
|
||||
}
|
||||
|
||||
Future<Model> _getCurrentModel() async {
|
||||
if (await isGrapheneOS()) {
|
||||
return Model.ggmlClip;
|
||||
} else {
|
||||
return Model.onnxClip;
|
||||
}
|
||||
}
|
||||
|
||||
void _startIndexing() {
|
||||
_logger.info("Start indexing");
|
||||
if (!_mlController.isCompleted) {
|
||||
_mlController.complete();
|
||||
}
|
||||
}
|
||||
|
||||
void _pauseIndexing() {
|
||||
if (_mlController.isCompleted) {
|
||||
_logger.info("Pausing indexing");
|
||||
_mlController = Completer<void>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<QueryResult> computeBulkScore(Map args) {
|
|
@ -27,7 +27,7 @@ class RemoteAssetsService {
|
|||
}
|
||||
|
||||
Future<String> _getLocalPath(String remotePath) async {
|
||||
return (await getTemporaryDirectory()).path +
|
||||
return (await getApplicationSupportDirectory()).path +
|
||||
"/assets/" +
|
||||
_urlToFileName(remotePath);
|
||||
}
|
||||
|
@ -53,5 +53,6 @@ class RemoteAssetsService {
|
|||
await existingFile.delete();
|
||||
}
|
||||
await NetworkClient.instance.getDio().download(url, savePath);
|
||||
_logger.info("Downloaded " + url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import "dart:math";
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/data/holidays.dart';
|
||||
import 'package:photos/data/months.dart';
|
||||
|
@ -17,14 +18,16 @@ import "package:photos/models/file/extensions/file_props.dart";
|
|||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import 'package:photos/models/search/album_search_result.dart';
|
||||
import 'package:photos/models/search/generic_search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/services/semantic_search/semantic_search_service.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import "package:photos/states/location_screen_state.dart";
|
||||
import "package:photos/ui/viewer/location/add_location_sheet.dart";
|
||||
import "package:photos/ui/viewer/location/location_screen.dart";
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
@ -676,17 +679,24 @@ class SearchService {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
//todo: remove this later, this hack is for interval+external evaluation
|
||||
// for suggestions
|
||||
final allCitiesSearch = query == '__city';
|
||||
if (allCitiesSearch) {
|
||||
query = '';
|
||||
}
|
||||
final results =
|
||||
await LocationService.instance.getFilesInCity(allFiles, query);
|
||||
for (final entry in results.entries) {
|
||||
final List<City> sortedByResultCount = results.keys.toList()
|
||||
..sort((a, b) => results[b]!.length.compareTo(results[a]!.length));
|
||||
for (final city in sortedByResultCount) {
|
||||
// If the location tag already exists for a city, don't add it again
|
||||
if (!locationTagNames.contains(entry.key.city)) {
|
||||
if (!locationTagNames.contains(city.city)) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
entry.key.city,
|
||||
entry.value,
|
||||
city.city,
|
||||
results[city]!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -701,6 +711,7 @@ class SearchService {
|
|||
final locationTagEntities =
|
||||
(await LocationService.instance.getLocationTags());
|
||||
final allFiles = await getAllFiles();
|
||||
final List<EnteFile> filesWithNoLocTag = [];
|
||||
|
||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||
if (limit != null && i >= limit) break;
|
||||
|
@ -709,15 +720,22 @@ class SearchService {
|
|||
|
||||
for (EnteFile file in allFiles) {
|
||||
if (file.hasLocation) {
|
||||
bool hasLocationTag = false;
|
||||
for (LocalEntity<LocationTag> tag in tagToItemsMap.keys) {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
file.location!,
|
||||
tag.item.radius,
|
||||
)) {
|
||||
hasLocationTag = true;
|
||||
tagToItemsMap[tag]!.add(file);
|
||||
}
|
||||
}
|
||||
// If the location tag already exists for a city, do not consider
|
||||
// it for the city suggestions
|
||||
if (!hasLocationTag) {
|
||||
filesWithNoLocTag.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -746,6 +764,30 @@ class SearchService {
|
|||
);
|
||||
}
|
||||
}
|
||||
if (limit == null || tagSearchResults.length < limit) {
|
||||
final results = await LocationService.instance
|
||||
.getFilesInCity(filesWithNoLocTag, '');
|
||||
final List<City> sortedByResultCount = results.keys.toList()
|
||||
..sort((a, b) => results[b]!.length.compareTo(results[a]!.length));
|
||||
for (final city in sortedByResultCount) {
|
||||
if (results[city]!.length <= 1) continue;
|
||||
tagSearchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.locationSuggestion,
|
||||
city.city,
|
||||
results[city]!,
|
||||
onResultTap: (ctx) {
|
||||
showAddLocationSheet(
|
||||
ctx,
|
||||
Location(latitude: city.lat, longitude: city.lng),
|
||||
name: city.city,
|
||||
radius: defaultCityRadius,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return tagSearchResults;
|
||||
} catch (e) {
|
||||
_logger.severe("Error in getAllLocationTags", e);
|
||||
|
|
|
@ -73,30 +73,39 @@ class UpdateService {
|
|||
return _latestVersion;
|
||||
}
|
||||
|
||||
Future<void> showUpdateNotification() async {
|
||||
if (!isIndependent()) {
|
||||
return;
|
||||
}
|
||||
Future<bool> shouldShowUpdateNoification() async {
|
||||
final shouldUpdate = await this.shouldUpdate();
|
||||
|
||||
final lastNotificationShownTime =
|
||||
_prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
final hasBeen3DaysSinceLastNotification =
|
||||
(now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
if (shouldUpdate &&
|
||||
hasBeen3DaysSinceLastNotification &&
|
||||
_latestVersion!.shouldNotify) {
|
||||
final hasBeenThresholdDaysSinceLastNotification =
|
||||
(now - lastNotificationShownTime) >
|
||||
((_latestVersion!.shouldNotify ? 1 : 3) * microSecondsInDay);
|
||||
|
||||
return shouldUpdate && hasBeenThresholdDaysSinceLastNotification;
|
||||
}
|
||||
|
||||
Future<void> showUpdateNotification() async {
|
||||
if (await shouldShowUpdateNoification()) {
|
||||
// ignore: unawaited_futures
|
||||
NotificationService.instance.showNotification(
|
||||
"Update available",
|
||||
"Click to install our best version yet",
|
||||
);
|
||||
await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
await resetUpdateAvailableShownTime();
|
||||
} else {
|
||||
_logger.info("Debouncing notification");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetUpdateAvailableShownTime() {
|
||||
return _prefs.setInt(
|
||||
kUpdateAvailableShownTimeKey,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
Future<LatestVersionInfo> _getLatestVersionInfo() async {
|
||||
final response = await NetworkClient.instance
|
||||
.getDio()
|
||||
|
@ -145,9 +154,12 @@ class UpdateService {
|
|||
);
|
||||
}
|
||||
return Platform.isAndroid
|
||||
? const Tuple2("play store", "market://details?id=io.ente.photos")
|
||||
? const Tuple2(
|
||||
"Google Play",
|
||||
"https://play.google.com/store/apps/details?id=io.ente.photos",
|
||||
)
|
||||
: const Tuple2(
|
||||
"app store",
|
||||
"App Store",
|
||||
"https://apps.apple.com/in/app/ente-photos/id1542026904",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,11 +14,14 @@ import "package:photos/utils/debouncer.dart";
|
|||
class LocationTagStateProvider extends StatefulWidget {
|
||||
final LocalEntity<LocationTag>? locationTagEntity;
|
||||
final Location? centerPoint;
|
||||
final double? radius;
|
||||
final Widget child;
|
||||
const LocationTagStateProvider(
|
||||
this.child, {
|
||||
this.centerPoint,
|
||||
this.locationTagEntity,
|
||||
// if the locationTagEntity is null, we use the centerPoint and radius
|
||||
this.radius,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -47,9 +50,12 @@ class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
|
|||
///If the location tag has a custom radius value, we add the custom radius
|
||||
///value to the list of default radius values only for this location tag and
|
||||
///keep it in the state of this widget.
|
||||
_radiusValues = _getRadiusValuesOfLocTag(_locationTagEntity?.item.radius);
|
||||
_radiusValues = _getRadiusValuesOfLocTag(
|
||||
_locationTagEntity?.item.radius ?? widget.radius,
|
||||
);
|
||||
|
||||
_selectedRadius = _locationTagEntity?.item.radius ?? defaultRadiusValue;
|
||||
_selectedRadius =
|
||||
_locationTagEntity?.item.radius ?? widget.radius ?? defaultRadiusValue;
|
||||
|
||||
_locTagEntityListener =
|
||||
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
||||
|
|
|
@ -42,7 +42,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
_passwordController.text = _volatilePassword!;
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => verifyPassword(_volatilePassword!),
|
||||
() => verifyPassword(_volatilePassword!),
|
||||
);
|
||||
}
|
||||
_passwordFocusNode.addListener(() {
|
||||
|
@ -100,69 +100,68 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
}
|
||||
|
||||
Future<void> verifyPassword(String password) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final dialog =
|
||||
createProgressDialog(context, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
password,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
_registerSRPForExistingUsers(kek).ignore();
|
||||
} on KeyDerivationError catch (e, s) {
|
||||
_logger.severe("Password verification failed", e, s);
|
||||
await dialog.hide();
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).recreatePasswordTitle,
|
||||
body: S.of(context).recreatePasswordBody,
|
||||
firstButtonLabel: S.of(context).useRecoveryKey,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const RecoveryPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Password verification failed", e, s);
|
||||
await dialog.hide();
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).incorrectPasswordTitle,
|
||||
body: S.of(context).pleaseTryAgain,
|
||||
firstButtonLabel: S.of(context).contactSupport,
|
||||
secondButtonLabel: S.of(context).ok,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await sendLogs(
|
||||
context,
|
||||
S.of(context).contactSupport,
|
||||
"support@ente.io",
|
||||
postShare: () {},
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
password,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
_registerSRPForExistingUsers(kek).ignore();
|
||||
} on KeyDerivationError catch (e, s) {
|
||||
_logger.severe("Password verification failed", e, s);
|
||||
await dialog.hide();
|
||||
Configuration.instance.setVolatilePassword(null);
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
unawaited(
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).recreatePasswordTitle,
|
||||
body: S.of(context).recreatePasswordBody,
|
||||
firstButtonLabel: S.of(context).useRecoveryKey,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
return const RecoveryPage();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Password verification failed", e, s);
|
||||
await dialog.hide();
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).incorrectPasswordTitle,
|
||||
body: S.of(context).pleaseTryAgain,
|
||||
firstButtonLabel: S.of(context).contactSupport,
|
||||
secondButtonLabel: S.of(context).ok,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await sendLogs(
|
||||
context,
|
||||
S.of(context).contactSupport,
|
||||
"support@ente.io",
|
||||
postShare: () {},
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await dialog.hide();
|
||||
Configuration.instance.setVolatilePassword(null);
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
unawaited(
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _registerSRPForExistingUsers(Uint8List key) async {
|
||||
|
@ -266,8 +265,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
@ -280,17 +279,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
),
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).forgotPassword,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).forgotPassword,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
|
@ -306,17 +301,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).changeEmail,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).changeEmail,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -60,7 +60,6 @@ class DynamicFAB extends StatelessWidget {
|
|||
} else {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: OutlinedButton(
|
||||
onPressed:
|
||||
|
|
|
@ -153,6 +153,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
final inheritedData = FullScreenMemoryData.of(context)!;
|
||||
final showStepProgressIndicator = inheritedData.memories.length < 60;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 84,
|
||||
|
|
|
@ -221,6 +221,7 @@ class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
|
|||
child: ThumbnailWidget(
|
||||
memory.file,
|
||||
shouldShowArchiveStatus: false,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("memories" + memory.file.tag),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -49,7 +49,11 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
_dialog = createProgressDialog(
|
||||
context,
|
||||
S.of(context).pleaseWait,
|
||||
isDismissible: true,
|
||||
);
|
||||
if (initPaymentUrl == null) {
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import "package:photos/ui/payment/store_subscription_page.dart";
|
||||
|
@ -9,18 +8,9 @@ StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) {
|
|||
if (UpdateService.instance.isIndependentFlavor()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
if (FeatureFlagService.instance.enableStripe() &&
|
||||
_isUserCreatedPostStripeSupport()) {
|
||||
if (FeatureFlagService.instance.enableStripe()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
} else {
|
||||
return StoreSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if the user was created after we added support for stripe payment
|
||||
// on frame. We do this check to avoid showing Stripe payment option for earlier
|
||||
// users who might have paid via playStore. This method should be removed once
|
||||
// we have better handling for active play/app store subscription & stripe plans.
|
||||
bool _isUserCreatedPostStripeSupport() {
|
||||
return Configuration.instance.getUserID()! > 1580559962386460;
|
||||
}
|
||||
|
|
|
@ -36,13 +36,9 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).selectYourPlan,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
Text(
|
||||
S.of(context).selectYourPlan,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
// import 'package:open_file/open_file.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
|
@ -63,32 +64,28 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
children: changelog,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: OutlinedButton(
|
||||
style: Theme.of(context).outlinedButtonTheme.style!.copyWith(
|
||||
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||
(Set<MaterialState> states) {
|
||||
return enteTextTheme.bodyBold;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
// ignore: unawaited_futures
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ApkDownloaderDialog(widget.latestVersionInfo);
|
||||
},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
S.of(context).update,
|
||||
),
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: S.of(context).update,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
// ignore: unawaited_futures
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ApkDownloaderDialog(widget.latestVersionInfo);
|
||||
},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: S.of(context).cancel,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
Center(
|
||||
|
|
|
@ -6,8 +6,8 @@ import "package:photos/core/event_bus.dart";
|
|||
import 'package:photos/events/embedding_updated_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.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";
|
||||
|
|
|
@ -198,9 +198,9 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
},
|
||||
);
|
||||
_initDeepLinks();
|
||||
UpdateService.instance.shouldUpdate().then((shouldUpdate) {
|
||||
if (shouldUpdate) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
UpdateService.instance.shouldShowUpdateNoification().then((value) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (value) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
|
@ -210,9 +210,11 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
);
|
||||
});
|
||||
}
|
||||
UpdateService.instance.resetUpdateAvailableShownTime();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// For sharing images coming from outside the app
|
||||
_initMediaShareSubscription();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
|
|
|
@ -73,6 +73,7 @@ class DetailPage extends StatefulWidget {
|
|||
class _DetailPageState extends State<DetailPage> {
|
||||
static const kLoadLimit = 100;
|
||||
final _logger = Logger("DetailPageState");
|
||||
bool _shouldDisableScroll = false;
|
||||
List<EnteFile>? _files;
|
||||
late PageController _pageController;
|
||||
final _selectedIndexNotifier = ValueNotifier(0);
|
||||
|
@ -171,6 +172,14 @@ class _DetailPageState extends State<DetailPage> {
|
|||
file,
|
||||
autoPlay: shouldAutoPlay(),
|
||||
tagPrefix: widget.config.tagPrefix,
|
||||
shouldDisableScroll: (value) {
|
||||
if (_shouldDisableScroll != value) {
|
||||
setState(() {
|
||||
_logger.fine('setState $_shouldDisableScroll to $value');
|
||||
_shouldDisableScroll = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
playbackCallback: (isPlaying) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_toggleFullScreen(shouldEnable: isPlaying);
|
||||
|
@ -199,7 +208,9 @@ class _DetailPageState extends State<DetailPage> {
|
|||
}
|
||||
_preloadEntries();
|
||||
},
|
||||
physics: const FastScrollPhysics(speedFactor: 4.0),
|
||||
physics: _shouldDisableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const FastScrollPhysics(speedFactor: 4.0),
|
||||
controller: _pageController,
|
||||
itemCount: _files!.length,
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
|
|||
class FileWidget extends StatelessWidget {
|
||||
final EnteFile file;
|
||||
final String? tagPrefix;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final Function(bool)? playbackCallback;
|
||||
final BoxDecoration? backgroundDecoration;
|
||||
final bool? autoPlay;
|
||||
|
@ -15,6 +16,7 @@ class FileWidget extends StatelessWidget {
|
|||
const FileWidget(
|
||||
this.file, {
|
||||
this.autoPlay,
|
||||
this.shouldDisableScroll,
|
||||
this.playbackCallback,
|
||||
this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
|
@ -30,6 +32,7 @@ class FileWidget extends StatelessWidget {
|
|||
file.fileType == FileType.image) {
|
||||
return ZoomableLiveImageNew(
|
||||
file,
|
||||
shouldDisableScroll: shouldDisableScroll,
|
||||
tagPrefix: tagPrefix,
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
key: key ?? ValueKey(fileKey),
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import "package:photo_view/photo_view_gallery.dart";
|
||||
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
|
@ -24,6 +23,7 @@ import 'package:photos/utils/thumbnail_util.dart';
|
|||
|
||||
class ZoomableImage extends StatefulWidget {
|
||||
final EnteFile photo;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
final bool shouldCover;
|
||||
|
@ -31,6 +31,7 @@ class ZoomableImage extends StatefulWidget {
|
|||
const ZoomableImage(
|
||||
this.photo, {
|
||||
Key? key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
this.shouldCover = false,
|
||||
|
@ -50,9 +51,9 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
bool _loadedLargeThumbnail = false;
|
||||
bool _loadingFinalImage = false;
|
||||
bool _loadedFinalImage = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
bool _isZooming = false;
|
||||
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -60,8 +61,12 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
_logger = Logger("ZoomableImage");
|
||||
_logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}');
|
||||
_scaleStateChangedCallback = (value) {
|
||||
if (widget.shouldDisableScroll != null) {
|
||||
widget.shouldDisableScroll!(value != PhotoViewScaleState.initial);
|
||||
}
|
||||
_isZooming = value != PhotoViewScaleState.initial;
|
||||
debugPrint("isZooming = $_isZooming, currentState $value");
|
||||
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||
};
|
||||
super.initState();
|
||||
}
|
||||
|
@ -82,41 +87,41 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
Widget content;
|
||||
|
||||
if (_imageProvider != null) {
|
||||
content = PhotoViewGallery.builder(
|
||||
gaplessPlayback: true,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||
builder: (context, index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: _imageProvider!,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
),
|
||||
controller: _photoViewController,
|
||||
);
|
||||
},
|
||||
itemCount: 1,
|
||||
content = PhotoViewGestureDetectorScope(
|
||||
axis: Axis.vertical,
|
||||
child: PhotoView(
|
||||
imageProvider: _imageProvider,
|
||||
controller: _photoViewController,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
: PhotoViewComputedScale.contained,
|
||||
gaplessPlayback: true,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
),
|
||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
content = const EnteLoadingWidget();
|
||||
}
|
||||
verticalDragCallback(d) => {
|
||||
if (!_isZooming)
|
||||
{
|
||||
if (d.delta.dy > dragSensitivity)
|
||||
{
|
||||
{Navigator.of(context).pop()},
|
||||
}
|
||||
else if (d.delta.dy < (dragSensitivity * -1))
|
||||
{
|
||||
showDetailsSheet(context, widget.photo),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
final GestureDragUpdateCallback? verticalDragCallback = _isZooming
|
||||
? null
|
||||
: (d) => {
|
||||
if (!_isZooming)
|
||||
{
|
||||
if (d.delta.dy > dragSensitivity)
|
||||
{
|
||||
{Navigator.of(context).pop()},
|
||||
}
|
||||
else if (d.delta.dy < (dragSensitivity * -1))
|
||||
{
|
||||
showDetailsSheet(context, widget.photo),
|
||||
},
|
||||
},
|
||||
};
|
||||
return GestureDetector(
|
||||
onVerticalDragUpdate: verticalDragCallback,
|
||||
child: content,
|
||||
|
@ -257,7 +262,9 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
required ImageProvider? previewImageProvider,
|
||||
required ImageProvider finalImageProvider,
|
||||
}) async {
|
||||
final bool shouldFixPosition = previewImageProvider != null && _isZooming;
|
||||
final bool shouldFixPosition = previewImageProvider != null &&
|
||||
_isZooming &&
|
||||
_photoViewController.scale != null;
|
||||
ImageInfo? finalImageInfo;
|
||||
if (shouldFixPosition) {
|
||||
final prevImageInfo = await getImageInfo(previewImageProvider);
|
||||
|
|
|
@ -16,12 +16,14 @@ import 'package:video_player/video_player.dart';
|
|||
|
||||
class ZoomableLiveImage extends StatefulWidget {
|
||||
final EnteFile enteFile;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
|
||||
const ZoomableLiveImage(
|
||||
this.enteFile, {
|
||||
Key? key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
}) : super(key: key);
|
||||
|
@ -43,9 +45,8 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
@override
|
||||
void initState() {
|
||||
_enteFile = widget.enteFile;
|
||||
_logger.info(
|
||||
'initState for ${_enteFile.generatedID} with tag ${_enteFile.tag} and name ${_enteFile.displayName}',
|
||||
);
|
||||
_logger.info('initState for ${_enteFile.generatedID} with tag ${_enteFile
|
||||
.tag} and name ${_enteFile.displayName}');
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -75,6 +76,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
content = ZoomableImage(
|
||||
_enteFile,
|
||||
tagPrefix: widget.tagPrefix,
|
||||
shouldDisableScroll: widget.shouldDisableScroll,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
);
|
||||
}
|
||||
|
@ -136,8 +138,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
}
|
||||
|
||||
Future<File?> _getLivePhotoVideo() async {
|
||||
if (_enteFile.isRemoteFile &&
|
||||
!(await isFileCached(_enteFile, liveVideo: true))) {
|
||||
if (_enteFile.isRemoteFile && !(await isFileCached(_enteFile, liveVideo: true))) {
|
||||
showShortToast(context, S.of(context).downloading);
|
||||
}
|
||||
|
||||
|
@ -205,4 +206,5 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,12 +17,14 @@ import 'package:photos/utils/toast_util.dart';
|
|||
|
||||
class ZoomableLiveImageNew extends StatefulWidget {
|
||||
final EnteFile enteFile;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
|
||||
const ZoomableLiveImageNew(
|
||||
this.enteFile, {
|
||||
Key? key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
}) : super(key: key);
|
||||
|
@ -79,6 +81,7 @@ class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
|
|||
content = ZoomableImage(
|
||||
_enteFile,
|
||||
tagPrefix: widget.tagPrefix,
|
||||
shouldDisableScroll: widget.shouldDisableScroll,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import "package:flutter/cupertino.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/files_db.dart";
|
||||
|
@ -21,6 +20,7 @@ import 'package:photos/models/gallery_type.dart';
|
|||
import "package:photos/models/metadata/common_keys.dart";
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||
|
@ -88,6 +88,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
late CollectionActions collectionActions;
|
||||
final GlobalKey shareButtonKey = GlobalKey();
|
||||
bool isQuickLink = false;
|
||||
late bool isInternalUser;
|
||||
late GalleryType galleryType;
|
||||
|
||||
@override
|
||||
|
@ -96,6 +97,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
_selectedFilesListener = () {
|
||||
setState(() {});
|
||||
};
|
||||
isInternalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
collectionActions = CollectionActions(CollectionsService.instance);
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
_userAuthEventSubscription =
|
||||
|
|
|
@ -22,14 +22,20 @@ import "package:photos/ui/viewer/location/radius_picker_widget.dart";
|
|||
|
||||
showAddLocationSheet(
|
||||
BuildContext context,
|
||||
Location coordinates,
|
||||
) {
|
||||
Location coordinates, {
|
||||
String name = '',
|
||||
double radius = defaultRadiusValue,
|
||||
}) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocationTagStateProvider(
|
||||
centerPoint: coordinates,
|
||||
const AddLocationSheet(),
|
||||
AddLocationSheet(
|
||||
radius: radius,
|
||||
name: name,
|
||||
),
|
||||
radius: radius,
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
|
@ -45,7 +51,13 @@ showAddLocationSheet(
|
|||
}
|
||||
|
||||
class AddLocationSheet extends StatefulWidget {
|
||||
const AddLocationSheet({super.key});
|
||||
final double radius;
|
||||
final String name;
|
||||
const AddLocationSheet({
|
||||
super.key,
|
||||
this.radius = defaultRadiusValue,
|
||||
this.name = '',
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
||||
|
@ -61,17 +73,20 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
||||
|
||||
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<double> _selectedRadiusNotifier =
|
||||
ValueNotifier(defaultRadiusValue);
|
||||
late ValueNotifier<double> _selectedRadiusNotifier;
|
||||
final _focusNode = FocusNode();
|
||||
final _textEditingController = TextEditingController();
|
||||
final _isEmptyNotifier = ValueNotifier(true);
|
||||
late final ValueNotifier<bool> _isEmptyNotifier;
|
||||
Widget? _keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_textEditingController.text = widget.name;
|
||||
_isEmptyNotifier = ValueNotifier(widget.name.isEmpty);
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
_selectedRadiusNotifier = ValueNotifier(widget.radius);
|
||||
_selectedRadiusNotifier.addListener(_selectedRadiusListener);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -155,11 +170,12 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
RadiusPickerWidget(
|
||||
_selectedRadiusNotifier,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
S.of(context).locationTagFeatureDescription,
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
if (widget.name.isEmpty) const SizedBox(height: 16),
|
||||
if (widget.name.isEmpty)
|
||||
Text(
|
||||
S.of(context).locationTagFeatureDescription,
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -131,6 +131,8 @@ class SearchResultWidget extends StatelessWidget {
|
|||
return "Day";
|
||||
case ResultType.location:
|
||||
return "Location";
|
||||
case ResultType.locationSuggestion:
|
||||
return "Add Location";
|
||||
case ResultType.fileType:
|
||||
return "Type";
|
||||
case ResultType.fileExtension:
|
||||
|
|
|
@ -3,6 +3,7 @@ import "dart:async";
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_animate/flutter_animate.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/extensions/list.dart";
|
||||
import "package:photos/models/search/album_search_result.dart";
|
||||
import "package:photos/models/search/generic_search_result.dart";
|
||||
import "package:photos/models/search/recent_searches.dart";
|
||||
|
@ -83,8 +84,6 @@ class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final sectionResults = snapshot.data!;
|
||||
sectionResults
|
||||
.sort((a, b) => a.name().compareTo(b.name()));
|
||||
return Text(sectionResults.length.toString())
|
||||
.animate()
|
||||
.fadeIn(
|
||||
|
@ -109,7 +108,15 @@ class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
|||
future: sectionData,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final sectionResults = snapshot.data!;
|
||||
List<SearchResult> sectionResults = snapshot.data!;
|
||||
sectionResults.sort((a, b) => a.name().compareTo(b.name()));
|
||||
if (widget.sectionType == SectionType.location) {
|
||||
final result = sectionResults.splitMatch(
|
||||
(e) => e.type() == ResultType.location,
|
||||
);
|
||||
sectionResults = result.matched;
|
||||
sectionResults.addAll(result.unmatched);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (sectionResults.length == index) {
|
||||
|
|
|
@ -77,7 +77,10 @@ class SearchableItemWidget extends StatelessWidget {
|
|||
children: [
|
||||
Text(
|
||||
searchResult.name(),
|
||||
style: textTheme.body,
|
||||
style: searchResult.type() ==
|
||||
ResultType.locationSuggestion
|
||||
? textTheme.bodyFaint
|
||||
: textTheme.body,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(
|
||||
|
|
|
@ -129,7 +129,6 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
child: Container(
|
||||
color: colorScheme.backgroundBase,
|
||||
child: Container(
|
||||
height: 44,
|
||||
color: colorScheme.fillFaint,
|
||||
child: TextFormField(
|
||||
controller: textController,
|
||||
|
|
|
@ -42,6 +42,14 @@ Future<bool> isLowSpecDevice() async {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future<bool> isGrapheneOS() async {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
return androidInfo.host.toLowerCase() == "grapheneos";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> isAndroidSDKVersionLowerThan(int inputSDK) async {
|
||||
if (Platform.isAndroid) {
|
||||
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
|
|
|
@ -2,10 +2,10 @@ import "package:dio/dio.dart";
|
|||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/services.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/button_result.dart';
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/ui/common/progress_dialog.dart';
|
||||
|
@ -91,7 +91,8 @@ String parseErrorForUI(
|
|||
}
|
||||
}
|
||||
// return generic error if the user is not internal and the error is not in debug mode
|
||||
if (!(isInternalUser && kDebugMode)) {
|
||||
if (!(FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
|
||||
kDebugMode)) {
|
||||
return genericError;
|
||||
}
|
||||
String errorInfo = "";
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:dio/dio.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
|
@ -34,6 +35,7 @@ import "package:photos/services/user_service.dart";
|
|||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_download_util.dart';
|
||||
import 'package:photos/utils/file_uploader_util.dart';
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import "package:uuid/uuid.dart";
|
||||
|
@ -69,6 +71,7 @@ class FileUploader {
|
|||
late ProcessType _processType;
|
||||
late bool _isBackground;
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
// _hasInitiatedForceUpload is used to track if user attempted force upload
|
||||
// where files are uploaded directly (without adding them to DB). In such
|
||||
// cases, we don't want to clear the stale upload files. See #removeStaleFiles
|
||||
|
@ -307,12 +310,36 @@ class FileUploader {
|
|||
return file.path.contains(kUploadTempPrefix) &&
|
||||
file.path.contains(".encrypted");
|
||||
});
|
||||
if (filesToDelete.isEmpty) {
|
||||
return;
|
||||
if (filesToDelete.isNotEmpty) {
|
||||
_logger.info('cleaning up state files ${filesToDelete.length}');
|
||||
for (final file in filesToDelete) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
_logger.info('cleaning up state files ${filesToDelete.length}');
|
||||
for (final file in filesToDelete) {
|
||||
await file.delete();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final sharedMediaDir =
|
||||
Configuration.instance.getSharedMediaDirectory() + "/";
|
||||
final sharedFiles = await Directory(sharedMediaDir).list().toList();
|
||||
if (sharedFiles.isNotEmpty) {
|
||||
_logger.info('Shared media directory cleanup ${sharedFiles.length}');
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
final existingLocalFileIDs =
|
||||
await FilesDB.instance.getExistingLocalFileIDs(ownerID);
|
||||
final Set<String> trackedSharedFilePaths = {};
|
||||
for (String localID in existingLocalFileIDs) {
|
||||
if (localID.contains(sharedMediaIdentifier)) {
|
||||
trackedSharedFilePaths
|
||||
.add(getSharedMediaPathFromLocalID(localID));
|
||||
}
|
||||
}
|
||||
for (final file in sharedFiles) {
|
||||
if (!trackedSharedFilePaths.contains(file.path)) {
|
||||
_logger.info('Deleting stale shared media file ${file.path}');
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to remove stale files", e, s);
|
||||
|
@ -431,7 +458,13 @@ class FileUploader {
|
|||
encryptedFilePath,
|
||||
key: key,
|
||||
);
|
||||
final thumbnailData = mediaUploadData.thumbnail;
|
||||
late final Uint8List? thumbnailData;
|
||||
if (mediaUploadData.thumbnail == null &&
|
||||
file.fileType == FileType.video) {
|
||||
thumbnailData = base64Decode(blackThumbnailBase64);
|
||||
} else {
|
||||
thumbnailData = mediaUploadData.thumbnail;
|
||||
}
|
||||
|
||||
final EncryptionResult encryptedThumbnailData =
|
||||
await CryptoUtil.encryptChaCha(
|
||||
|
@ -493,17 +526,21 @@ class FileUploader {
|
|||
CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!);
|
||||
final keyDecryptionNonce =
|
||||
CryptoUtil.bin2base64(encryptedFileKeyData.nonce!);
|
||||
final Map<String, dynamic> pubMetadata = {};
|
||||
MetadataRequest? pubMetadataRequest;
|
||||
if ((mediaUploadData.height ?? 0) != 0 &&
|
||||
(mediaUploadData.width ?? 0) != 0) {
|
||||
final pubMetadata = {
|
||||
heightKey: mediaUploadData.height,
|
||||
widthKey: mediaUploadData.width,
|
||||
};
|
||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||
pubMetadata[motionVideoIndexKey] =
|
||||
mediaUploadData.motionPhotoStartIndex;
|
||||
}
|
||||
pubMetadata[heightKey] = mediaUploadData.height;
|
||||
pubMetadata[widthKey] = mediaUploadData.width;
|
||||
}
|
||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||
pubMetadata[motionVideoIndexKey] =
|
||||
mediaUploadData.motionPhotoStartIndex;
|
||||
}
|
||||
if (mediaUploadData.thumbnail == null) {
|
||||
pubMetadata[noThumbKey] = true;
|
||||
}
|
||||
if (pubMetadata.isNotEmpty) {
|
||||
pubMetadataRequest = await getPubMetadataRequest(
|
||||
file,
|
||||
pubMetadata,
|
||||
|
|
|
@ -208,6 +208,10 @@ Future<Uint8List?> _getThumbnailForUpload(
|
|||
quality: thumbnailQuality,
|
||||
);
|
||||
if (thumbnailData == null) {
|
||||
// allow videos to be uploaded without thumbnails
|
||||
if (asset.type == AssetType.video) {
|
||||
return null;
|
||||
}
|
||||
throw InvalidFileError(
|
||||
"no thumbnail : ${file.fileType} ${file.tag}",
|
||||
InvalidReason.thumbnailMissing,
|
||||
|
@ -227,6 +231,10 @@ Future<Uint8List?> _getThumbnailForUpload(
|
|||
final String errMessage =
|
||||
"thumbErr for ${file.fileType}, ${extension(file.displayName)} ${file.tag}";
|
||||
_logger.warning(errMessage, e);
|
||||
// allow videos to be uploaded without thumbnails
|
||||
if (asset.type == AssetType.video) {
|
||||
return null;
|
||||
}
|
||||
throw InvalidFileError(errMessage, InvalidReason.thumbnailMissing);
|
||||
}
|
||||
}
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -96,7 +96,15 @@ packages:
|
|||
sha256: "34550cf9b383e5a1844e7d22119aa500508c7df9421fa967c9fb4430d6cb2878"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.2.1"
|
||||
battery_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: battery_info
|
||||
sha256: "5d5249c87a600a0a20d6b2f5ffdf90d711bccb1bfd3a58e5a6228f270031c680"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
bip39:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1389,8 +1397,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: isolates
|
||||
resolved-ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d"
|
||||
ref: HEAD
|
||||
resolved-ref: "1318dce97f3aae5ec9bdf7491d5eff0ad6beb378"
|
||||
url: "https://github.com/ente-io/onnxruntime.git"
|
||||
source: git
|
||||
version: "1.1.0"
|
||||
|
|
|
@ -12,7 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.57+577
|
||||
version: 0.8.61+581
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
@ -24,6 +24,7 @@ dependencies:
|
|||
animated_list_plus: ^0.4.5
|
||||
archive: ^3.1.2
|
||||
background_fetch: ^1.2.1
|
||||
battery_info: ^1.1.1
|
||||
bip39: ^1.0.6
|
||||
cached_network_image: ^3.0.0
|
||||
chewie:
|
||||
|
@ -120,9 +121,7 @@ dependencies:
|
|||
|
||||
# open_file: ^3.2.1
|
||||
onnxruntime:
|
||||
git:
|
||||
url: "https://github.com/ente-io/onnxruntime.git"
|
||||
ref: "isolates"
|
||||
git: "https://github.com/ente-io/onnxruntime.git"
|
||||
open_mail_app: ^0.4.5
|
||||
package_info_plus: ^4.1.0
|
||||
page_transition: ^2.0.2
|
||||
|
|
Loading…
Reference in a new issue