Merge branch 'main' into text_embeddings_cache
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
211
mobile/apps/photos/lib/ui/sharing/qr_code_dialog_widget.dart
Normal file
211
mobile/apps/photos/lib/ui/sharing/qr_code_dialog_widget.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user