Merge branch 'main' into text_embeddings_cache

This commit is contained in:
Laurens Priem
2025-09-10 17:51:51 +05:30
committed by GitHub
14 changed files with 357 additions and 17 deletions

View File

@@ -221,15 +221,14 @@ class SuperLogging {
// Initialize log viewer integration in debug mode
// Initialize log viewer in debug mode only
if (kDebugMode) {
try {
await LogViewer.initialize();
// Register LogViewer with SuperLogging to receive logs with process prefix
LogViewer.registerWithSuperLogging(SuperLogging.registerLogCallback);
$.info("Log viewer initialized successfully");
} catch (e) {
$.warning("Failed to initialize log viewer: $e");
}
if (_preferences.getBool("enable_db_logging") ?? kDebugMode) {
try {
await LogViewer.initialize();
// Register LogViewer with SuperLogging to receive logs with process prefix
LogViewer.registerWithSuperLogging(SuperLogging.registerLogCallback);
$.info("Log viewer initialized successfully");
} catch (e) {
$.warning("Failed to initialize log viewer: $e");
}
if (appConfig.body == null) return;

View File

@@ -23,6 +23,7 @@ class DebugSectionWidget extends StatefulWidget {
}
class _DebugSectionWidgetState extends State<DebugSectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
@@ -35,6 +36,27 @@ class _DebugSectionWidgetState extends State<DebugSectionWidget> {
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Enable database logging",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: ToggleSwitchWidget(
value: () => localSettings.enableDatabaseLogging,
onChanged: () async {
final newValue = !localSettings.enableDatabaseLogging;
await localSettings.setEnableDatabaseLogging(newValue);
setState(() {});
showShortToast(
context,
newValue
? "Database logging enabled. Restart app."
: "Database logging disabled. Restart app.",
);
},
),
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(

View File

@@ -36,6 +36,7 @@ class SettingsPage extends StatelessWidget {
const SettingsPage({super.key, required this.emailNotifier});
@override
Widget build(BuildContext context) {
Bus.instance.fire(OpenedSettingsEvent());
@@ -82,7 +83,7 @@ class SettingsPage extends StatelessWidget {
),
),
),
if (kDebugMode)
if (localSettings.enableDatabaseLogging)
GestureDetector(
onTap: () {
Navigator.of(context).push(

View File

@@ -7,6 +7,7 @@ import "package:flutter/services.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/api/collection/public_url.dart";
import 'package:photos/models/collection/collection.dart';
import 'package:photos/service_locator.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
@@ -19,6 +20,7 @@ import "package:photos/ui/components/toggle_switch_widget.dart";
import 'package:photos/ui/notification/toast.dart';
import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart';
import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart';
import 'package:photos/ui/sharing/qr_code_dialog_widget.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
import "package:photos/utils/share_util.dart";
@@ -291,6 +293,32 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
);
},
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: flagService.internalUser,
),
if (!url.isExpired && flagService.internalUser)
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
if (!url.isExpired && flagService.internalUser)
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Send QR Code (i)",
makeTextBold: true,
),
leadingIcon: Icons.qr_code_outlined,
menuItemColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return QrCodeDialogWidget(
collection: widget.collection!,
);
},
);
},
isTopBorderRadiusRemoved: true,
),
const SizedBox(
height: 24,

View File

@@ -0,0 +1,211 @@
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/models/collection/collection.dart';
import 'package:photos/services/collections_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:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
class QrCodeDialogWidget extends StatefulWidget {
final Collection collection;
const QrCodeDialogWidget({
super.key,
required this.collection,
});
@override
State<QrCodeDialogWidget> createState() => _QrCodeDialogWidgetState();
}
class _QrCodeDialogWidgetState extends State<QrCodeDialogWidget> {
final GlobalKey _qrKey = GlobalKey();
Future<void> _shareQrCode() async {
try {
final RenderRepaintBoundary boundary =
_qrKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
final ui.Image image = await boundary.toImage(pixelRatio: 3.0);
final ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData != null) {
final Uint8List pngBytes = byteData.buffer.asUint8List();
final directory = await getTemporaryDirectory();
final file = File(
'${directory.path}/ente_qr_${widget.collection.displayName}.png',
);
await file.writeAsBytes(pngBytes);
await Share.shareXFiles(
[XFile(file.path)],
text:
'Scan this QR code to view my ${widget.collection.displayName} album on ente',
);
// Close the dialog after sharing is initiated
if (mounted) {
Navigator.of(context).pop();
}
}
} catch (e) {
debugPrint('Error sharing QR code: $e');
}
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final double qrSize = min(screenWidth - 80, 300.0);
final enteTextTheme = getEnteTextTheme(context);
final enteColorScheme = getEnteColorScheme(context);
// Get the public URL for the collection
final String publicUrl =
CollectionsService.instance.getPublicUrl(widget.collection);
// Get album name, truncate if too long
final String albumName = widget.collection.displayName.length > 30
? '${widget.collection.displayName.substring(0, 27)}...'
: widget.collection.displayName;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: enteColorScheme.backgroundBase,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header with close button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"QR Code",
style: enteTextTheme.largeBold,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
color: enteColorScheme.strokeBase,
),
],
),
const SizedBox(height: 8),
// QR Code with RepaintBoundary for sharing
RepaintBoundary(
key: _qrKey,
child: Container(
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
// Album name at top center (inside border) - Reduced size
Text(
albumName,
style: enteTextTheme.bodyBold.copyWith(
color: Colors.black87,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
// QR Code with better spacing
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.shade100,
width: 1,
),
),
child: QrImageView(
data: publicUrl,
version: QrVersions.auto,
size: qrSize - 100,
eyeStyle: const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Colors.black,
),
dataModuleStyle: const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Colors.black,
),
errorCorrectionLevel: QrErrorCorrectLevel.M,
),
),
const SizedBox(height: 24),
],
),
// Ente branding at bottom right (inside border) - Fixed positioning
Positioned(
bottom: -2,
right: 2,
child: Text(
'ente',
style: enteTextTheme.small.copyWith(
color: enteColorScheme.primary700,
fontSize: 14,
letterSpacing: 1.2,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
const SizedBox(height: 24),
// Share button
ButtonWidget(
buttonType: ButtonType.primary,
icon: Icons.adaptive.share,
labelText: "Share",
onTap: _shareQrCode,
shouldSurfaceExecutionStates: false,
),
],
),
),
);
}
}

View File

@@ -1,10 +1,12 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "package:photos/extensions/user_extension.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/api/collection/user.dart";
import 'package:photos/models/collection/collection.dart';
import 'package:photos/service_locator.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
@@ -19,6 +21,7 @@ import 'package:photos/ui/sharing/album_participants_page.dart';
import "package:photos/ui/sharing/album_share_info_widget.dart";
import "package:photos/ui/sharing/manage_album_participant.dart";
import 'package:photos/ui/sharing/manage_links_widget.dart';
import 'package:photos/ui/sharing/qr_code_dialog_widget.dart';
import 'package:photos/ui/sharing/user_avator_widget.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:photos/utils/share_util.dart';
@@ -214,8 +217,34 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
);
},
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: flagService.internalUser,
),
if (flagService.internalUser)
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
if (flagService.internalUser)
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Send QR Code (i)",
makeTextBold: true,
),
leadingIcon: Icons.qr_code_outlined,
menuItemColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return QrCodeDialogWidget(
collection: widget.collection,
);
},
);
},
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: true,
),
],
);
}

View File

@@ -1,3 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/ui/viewer/gallery/component/group/type.dart';
import "package:photos/utils/ram_check_util.dart";
@@ -42,6 +44,7 @@ class LocalSettings {
static const kCollectionViewType = "collection_view_type";
static const kCollectionSortDirection = "collection_sort_direction";
static const kShowLocalIDOverThumbnails = "show_local_id_over_thumbnails";
static const kEnableDatabaseLogging = "enable_db_logging";
// Thumbnail queue configuration keys
static const kSmallQueueMaxConcurrent = "small_queue_max_concurrent";
@@ -234,6 +237,13 @@ class LocalSettings {
await _prefs.setBool(kShowLocalIDOverThumbnails, value);
}
bool get enableDatabaseLogging =>
_prefs.getBool(kEnableDatabaseLogging) ?? kDebugMode;
Future<void> setEnableDatabaseLogging(bool value) async {
await _prefs.setBool(kEnableDatabaseLogging, value);
}
// Thumbnail queue configuration - Small queue
int get smallQueueMaxConcurrent =>
_prefs.getInt(kSmallQueueMaxConcurrent) ?? 15;

View File

@@ -177,6 +177,7 @@ dependencies:
url: https://github.com/ente-io/privacy_screen.git
ref: v2-only
pro_image_editor: ^6.0.0
qr_flutter: ^4.1.0
receive_sharing_intent: # pub.dev is behind
git:
url: https://github.com/KasemJaffer/receive_sharing_intent.git

View File

@@ -1,2 +1,3 @@
- Laurens: text embedding caching for memories and discover
- Neeraj: (i) Debug option to enable logViewer
- Neeraj: Potential fix for ios in-app payment

View File

@@ -858,7 +858,12 @@ func runServer(environment string, server *gin.Engine) {
log.Fatal(server.RunTLS(":443", certPath, keyPath))
} else {
server.Run(":8080")
port := 8080
if viper.IsSet("http.port") {
port = viper.GetInt("http.port")
}
log.Infof("starting server on port %d", port)
server.Run(fmt.Sprintf(":%d", port))
}
}

View File

@@ -71,8 +71,13 @@ log-file: ""
# HTTP connection parameters
http:
# If true, bind to 443 and use TLS.
# By default, this is false, and museum will bind to 8080 without TLS.
# The port to bind to.
# If not specified, defaults to 8080 (HTTP) or 443 (HTTPS with use-tls: true)
# port: 8080
# If true, use TLS for HTTPS connections.
# When true and port is not specified, defaults to port 443.
# When false and port is not specified, defaults to port 8080.
# use-tls: true
# Specify the base endpoints for various apps

View File

@@ -3,6 +3,7 @@ package ente
import (
"fmt"
"github.com/ente-io/stacktrace"
"golang.org/x/net/idna"
"regexp"
"strings"
)
@@ -139,8 +140,17 @@ func isValidDomainWithoutScheme(input string) error {
if strings.Contains(trimmed, "://") {
return NewBadRequestWithMessage("domain should not contain scheme (e.g., http:// or https://)")
}
if !domainRegex.MatchString(trimmed) {
// Convert IDN to ASCII (Punycode) for validation
asciiDomain, err := idna.ToASCII(trimmed)
if err != nil {
return NewBadRequestWithMessage(fmt.Sprintf("invalid idn domain format: %s", trimmed))
}
// Validate the ASCII version
if !domainRegex.MatchString(asciiDomain) {
return NewBadRequestWithMessage(fmt.Sprintf("invalid domain format: %s", trimmed))
}
return nil
}

View File

@@ -11,7 +11,9 @@ func TestIsValidDomainWithoutScheme(t *testing.T) {
// ✅ Valid cases
{"simple domain", "google.com", false},
{"multi-level domain", "sub.example.co.in", false},
{"multi-level domain", "photos.ä.com", false},
{"numeric in label", "a1b2c3.com", false},
{"idn", "テスト.jp", false},
{"long but valid label", "my-very-long-subdomain-name.example.com", false},
// ❌ Leading/trailing spaces

View File

@@ -5,6 +5,7 @@ import (
"context"
"crypto/sha256"
"fmt"
"golang.org/x/net/idna"
"net/http"
"net/url"
"strings"
@@ -220,11 +221,26 @@ func (m *CollectionLinkMiddleware) validateOrigin(c *gin.Context, ownerID int64)
m.DiscordController.NotifyPotentialAbuse(alertMessage + " - originParseFailed")
return nil
}
if !strings.Contains(strings.ToLower(parse.Host), strings.ToLower(*domain)) {
logger.Warnf("domainMismatch for owner %d, origin %s, domain %s host %s", ownerID, origin, *domain, parse.Host)
unicodeDomain, err := idna.ToUnicode(*domain)
if err != nil {
logger.WithError(err).Error("domainToUnicodeFailed")
m.DiscordController.NotifyPotentialAbuse(alertMessage + " - domainToUnicodeFailed")
return nil
}
if !strings.Contains(strings.ToLower(parse.Host), strings.ToLower(*domain)) && !strings.Contains(strings.ToLower(parse.Host), strings.ToLower(unicodeDomain)) {
logger.Warnf("domainMismatch: domain %s (unicode %s) vs originHost %s", *domain, unicodeDomain, parse.Host)
m.DiscordController.NotifyPotentialAbuse(alertMessage + " - domainMismatch")
return ente.NewPermissionDeniedError("unknown custom domain")
}
// Additional exact match check. In the future, remove the contains check above and only keep this exact match check.
if !strings.EqualFold(parse.Host, *domain) && !strings.EqualFold(parse.Host, unicodeDomain) {
logger.Warnf("exactDomainMismatch: domain %s (unicode %s) vs originHost %s", *domain, unicodeDomain, parse.Host)
m.DiscordController.NotifyPotentialAbuse(alertMessage + " - exactDomainMismatch")
// Do not return error here till we are fully sure that this won't cause any issues for existing
// custom domains.
// return ente.NewPermissionDeniedError("unknown custom domain")
}
return nil
}