Merge remote-tracking branch 'refs/remotes/origin/homewidget' into homewidget

This commit is contained in:
Prateek Sunal 2024-02-22 17:42:02 +05:30
commit c5ce98971f
70 changed files with 2924 additions and 459 deletions

View file

@ -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
View file

@ -5,6 +5,3 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Signing config files
*.jks

View file

@ -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.

View 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.

View file

@ -0,0 +1 @@
ente é um aplicativo de armazenamento de fotos criptografado de ponta a ponta

View file

@ -0,0 +1 @@
ente - armazenamento criptografado de fotos

View 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

View file

@ -0,0 +1 @@
fotos,fotografia,família,privacidade,nuvem,backup,vídeos,foto,criptografia,armazenamento,álbum,alternativa

View file

@ -0,0 +1 @@
ente Fotos

View file

@ -0,0 +1 @@
Armazenamento de fotos criptografado

View file

@ -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

View 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.

View file

@ -0,0 +1 @@
Armazenamento de fotos criptografado - backup, organize e compartilhe suas fotos e vídeos

View file

@ -0,0 +1 @@
ente Fotos

View file

@ -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

View file

@ -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",

View file

@ -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();
}

View file

@ -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';

View file

@ -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=';

View file

@ -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 =

View file

@ -0,0 +1,7 @@
import "package:photos/events/event.dart";
class MachineLearningControlEvent extends Event {
final bool shouldRun;
MachineLearningControlEvent(this.shouldRun);
}

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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],
);
}
}

View file

@ -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);

View file

@ -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 {

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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/";

View file

@ -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,

View file

@ -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/";

View file

@ -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";

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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",
);
}

View file

@ -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) {

View file

@ -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,
),
),
),
],

View file

@ -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:

View file

@ -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,

View file

@ -221,6 +221,7 @@ class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
child: ThumbnailWidget(
memory.file,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
key: Key("memories" + memory.file.tag),
),
),

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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(

View file

@ -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(

View file

@ -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";

View file

@ -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(

View file

@ -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,
);

View file

@ -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),

View file

@ -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);

View file

@ -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>
}
});
}
}

View file

@ -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,
);
}

View file

@ -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 =

View file

@ -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,
),
],
),
),

View file

@ -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:

View file

@ -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) {

View file

@ -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(

View file

@ -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,

View file

@ -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;

View file

@ -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 = "";

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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"

View file

@ -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