[mob][photos] Merge main
This commit is contained in:
commit
b494c308b1
45 changed files with 768 additions and 276 deletions
|
@ -20,7 +20,6 @@
|
|||
"codeIssuerHint": "Issuer",
|
||||
"codeSecretKeyHint": "Secret Key",
|
||||
"codeAccountHint": "Account (you@domain.com)",
|
||||
"accountKeyType": "Type of key",
|
||||
"sessionExpired": "Session expired",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
|
|
|
@ -37,6 +37,7 @@ import 'package:window_manager/window_manager.dart';
|
|||
final _logger = Logger("main");
|
||||
|
||||
Future<void> initSystemTray() async {
|
||||
if (PlatformUtil.isMobile()) return;
|
||||
String path = Platform.isWindows
|
||||
? 'assets/icons/auth-icon.ico'
|
||||
: 'assets/icons/auth-icon.png';
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:ente_auth/utils/totp_util.dart';
|
|||
|
||||
class Code {
|
||||
static const defaultDigits = 6;
|
||||
static const steamDigits = 5;
|
||||
static const defaultPeriod = 30;
|
||||
|
||||
int? generatedID;
|
||||
|
@ -57,36 +58,42 @@ class Code {
|
|||
updatedAlgo,
|
||||
updatedType,
|
||||
updatedCounter,
|
||||
"otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
|
||||
"otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}"
|
||||
"&digits=$updatedDigits&issuer=$updateIssuer"
|
||||
"&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
|
||||
generatedID: generatedID,
|
||||
);
|
||||
}
|
||||
|
||||
static Code fromAccountAndSecret(
|
||||
Type type,
|
||||
String account,
|
||||
String issuer,
|
||||
String secret,
|
||||
int digits,
|
||||
) {
|
||||
return Code(
|
||||
account,
|
||||
issuer,
|
||||
defaultDigits,
|
||||
digits,
|
||||
defaultPeriod,
|
||||
secret,
|
||||
Algorithm.sha1,
|
||||
Type.totp,
|
||||
type,
|
||||
0,
|
||||
"otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret",
|
||||
"otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$issuer&period=30&secret=$secret",
|
||||
);
|
||||
}
|
||||
|
||||
static Code fromRawData(String rawData) {
|
||||
Uri uri = Uri.parse(rawData);
|
||||
final issuer = _getIssuer(uri);
|
||||
|
||||
try {
|
||||
return Code(
|
||||
_getAccount(uri),
|
||||
_getIssuer(uri),
|
||||
_getDigits(uri),
|
||||
issuer,
|
||||
_getDigits(uri, issuer),
|
||||
_getPeriod(uri),
|
||||
getSanitizedSecret(uri.queryParameters['secret']!),
|
||||
_getAlgorithm(uri),
|
||||
|
@ -140,10 +147,13 @@ class Code {
|
|||
}
|
||||
}
|
||||
|
||||
static int _getDigits(Uri uri) {
|
||||
static int _getDigits(Uri uri, String issuer) {
|
||||
try {
|
||||
return int.parse(uri.queryParameters['digits']!);
|
||||
} catch (e) {
|
||||
if (issuer.toLowerCase() == "steam") {
|
||||
return steamDigits;
|
||||
}
|
||||
return defaultDigits;
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +196,8 @@ class Code {
|
|||
static Type _getType(Uri uri) {
|
||||
if (uri.host == "totp") {
|
||||
return Type.totp;
|
||||
} else if (uri.host == "steam") {
|
||||
return Type.steam;
|
||||
} else if (uri.host == "hotp") {
|
||||
return Type.hotp;
|
||||
}
|
||||
|
@ -223,6 +235,9 @@ class Code {
|
|||
enum Type {
|
||||
totp,
|
||||
hotp,
|
||||
steam;
|
||||
|
||||
bool get isTOTPCompatible => this == totp || this == steam;
|
||||
}
|
||||
|
||||
enum Algorithm {
|
||||
|
|
|
@ -61,6 +61,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeIssuerHint,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||
labelText: l10n.codeIssuerHint,
|
||||
),
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
|
@ -78,6 +80,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeSecretKeyHint,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||
labelText: l10n.codeSecretKeyHint,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
|
@ -105,12 +109,12 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeAccountHint,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||
labelText: l10n.codeAccountHint,
|
||||
),
|
||||
controller: _accountController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: OutlinedButton(
|
||||
|
@ -152,6 +156,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
final account = _accountController.text.trim();
|
||||
final issuer = _issuerController.text.trim();
|
||||
final secret = _secretController.text.trim().replaceAll(' ', '');
|
||||
final isStreamCode = issuer.toLowerCase() == "steam";
|
||||
if (widget.code != null && widget.code!.secret != secret) {
|
||||
ButtonResult? result = await showChoiceActionSheet(
|
||||
context,
|
||||
|
@ -168,9 +173,11 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
}
|
||||
final Code newCode = widget.code == null
|
||||
? Code.fromAccountAndSecret(
|
||||
isStreamCode ? Type.steam : Type.totp,
|
||||
account,
|
||||
issuer,
|
||||
secret,
|
||||
isStreamCode ? Code.steamDigits : Code.defaultDigits,
|
||||
)
|
||||
: widget.code!.copyWith(
|
||||
account: account,
|
||||
|
|
|
@ -53,7 +53,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
String newCode = _getCurrentOTP();
|
||||
if (newCode != _currentCode.value) {
|
||||
_currentCode.value = newCode;
|
||||
if (widget.code.type == Type.totp) {
|
||||
if (widget.code.type.isTOTPCompatible) {
|
||||
_nextCode.value = _getNextTotp();
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
_shouldShowLargeIcon = PreferenceService.instance.shouldShowLargeIcons();
|
||||
if (!_isInitialized) {
|
||||
_currentCode.value = _getCurrentOTP();
|
||||
if (widget.code.type == Type.totp) {
|
||||
if (widget.code.type.isTOTPCompatible) {
|
||||
_nextCode.value = _getNextTotp();
|
||||
}
|
||||
_isInitialized = true;
|
||||
|
@ -213,7 +213,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.code.type == Type.totp)
|
||||
if (widget.code.type.isTOTPCompatible)
|
||||
CodeTimerProgress(
|
||||
period: widget.code.period,
|
||||
),
|
||||
|
@ -263,7 +263,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
},
|
||||
),
|
||||
),
|
||||
widget.code.type == Type.totp
|
||||
widget.code.type.isTOTPCompatible
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
_copyNextToClipboard();
|
||||
|
@ -481,7 +481,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
|
||||
String _getNextTotp() {
|
||||
try {
|
||||
assert(widget.code.type == Type.totp);
|
||||
assert(widget.code.type.isTOTPCompatible);
|
||||
return getNextTotp(widget.code);
|
||||
} catch (e) {
|
||||
return context.l10n.error;
|
||||
|
|
|
@ -92,9 +92,11 @@ Future<int?> _processBitwardenExportFile(
|
|||
var account = item['login']['username'];
|
||||
|
||||
code = Code.fromAccountAndSecret(
|
||||
Type.totp,
|
||||
account,
|
||||
issuer,
|
||||
totp,
|
||||
Code.defaultDigits,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:otp/otp.dart' as otp;
|
||||
|
||||
String getOTP(Code code) {
|
||||
if(code.type == Type.hotp) {
|
||||
if (code.type == Type.hotp) {
|
||||
return _getHOTPCode(code);
|
||||
}
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
|
@ -60,4 +60,4 @@ String safeDecode(String value) {
|
|||
debugPrint("Failed to decode $e");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 2.0.56+256
|
||||
version: 2.0.57+257
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
|
19
desktop/.github/workflows/desktop-release.yml
vendored
19
desktop/.github/workflows/desktop-release.yml
vendored
|
@ -32,9 +32,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
# Commented for testing
|
||||
# os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
@ -55,13 +53,6 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Prepare for app notarization
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
# Import Apple API key for app notarization on macOS
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
echo '${{ secrets.API_KEY }}' > ~/private_keys/AuthKey_${{ secrets.API_KEY_ID }}.p8
|
||||
|
||||
- name: Install libarchive-tools for pacman build
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
# See:
|
||||
|
@ -84,7 +75,9 @@ jobs:
|
|||
mac_certs: ${{ secrets.MAC_CERTS }}
|
||||
mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
env:
|
||||
# macOS notarization API key details
|
||||
API_KEY_ID: ${{ secrets.API_KEY_ID }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
# macOS notarization credentials key details
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD:
|
||||
${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
USE_HARD_LINKS: false
|
||||
|
|
|
@ -23,10 +23,10 @@ The workflow is:
|
|||
|
||||
- Update the CHANGELOG.
|
||||
- Update the version in `package.json`
|
||||
- `git commit -m 'Release v1.x.x'`
|
||||
- `git commit -m "[photosd] Release v1.2.3"`
|
||||
- Open PR, merge into main.
|
||||
|
||||
2. Tag this commit with a tag matching the pattern `photosd-v1.2.3`, where
|
||||
2. Tag the merge commit with a tag matching the pattern `photosd-v1.2.3`, where
|
||||
`1.2.3` is the version in `package.json`
|
||||
|
||||
```sh
|
||||
|
|
|
@ -29,4 +29,5 @@ mac:
|
|||
arch: [universal]
|
||||
category: public.app-category.photography
|
||||
hardenedRuntime: true
|
||||
notarize: true
|
||||
afterSign: electron-builder-notarize
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"version": "1.7.0-beta.0",
|
||||
"private": true,
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"repository": "github:ente-io/photos-desktop",
|
||||
"author": "Ente <code@ente.io>",
|
||||
"main": "app/main.js",
|
||||
"scripts": {
|
||||
|
@ -44,7 +45,7 @@
|
|||
"@typescript-eslint/parser": "^7",
|
||||
"concurrently": "^8",
|
||||
"electron": "^30",
|
||||
"electron-builder": "^24",
|
||||
"electron-builder": "25.0.0-alpha.6",
|
||||
"electron-builder-notarize": "^1.5",
|
||||
"eslint": "^8",
|
||||
"prettier": "^3",
|
||||
|
|
File diff suppressed because it is too large
Load diff
6
mobile/lib/generated/intl/messages_en.dart
generated
6
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -362,8 +362,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"autoCastiOSPermission": MessageLookupByLibrary.simpleMessage(
|
||||
"Make sure Local Network permissions are turned on for the Ente Photos app, in Settings."),
|
||||
"autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"),
|
||||
"autoPairGoogle": MessageLookupByLibrary.simpleMessage(
|
||||
"Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos."),
|
||||
"autoPairDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Auto pair works only with devices that support Chromecast."),
|
||||
"available": MessageLookupByLibrary.simpleMessage("Available"),
|
||||
"backedUpFolders":
|
||||
MessageLookupByLibrary.simpleMessage("Backed up folders"),
|
||||
|
@ -918,7 +918,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"manageSubscription":
|
||||
MessageLookupByLibrary.simpleMessage("Manage subscription"),
|
||||
"manualPairDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Pair with PIN works for any large screen device you want to play your album on."),
|
||||
"Pair with PIN works with any screen you wish to view your album on."),
|
||||
"map": MessageLookupByLibrary.simpleMessage("Map"),
|
||||
"maps": MessageLookupByLibrary.simpleMessage("Maps"),
|
||||
"mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
|
||||
|
|
12
mobile/lib/generated/l10n.dart
generated
12
mobile/lib/generated/l10n.dart
generated
|
@ -8594,20 +8594,20 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.`
|
||||
String get autoPairGoogle {
|
||||
/// `Auto pair works only with devices that support Chromecast.`
|
||||
String get autoPairDesc {
|
||||
return Intl.message(
|
||||
'Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.',
|
||||
name: 'autoPairGoogle',
|
||||
'Auto pair works only with devices that support Chromecast.',
|
||||
name: 'autoPairDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Pair with PIN works for any large screen device you want to play your album on.`
|
||||
/// `Pair with PIN works with any screen you wish to view your album on.`
|
||||
String get manualPairDesc {
|
||||
return Intl.message(
|
||||
'Pair with PIN works for any large screen device you want to play your album on.',
|
||||
'Pair with PIN works with any screen you wish to view your album on.',
|
||||
name: 'manualPairDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
|
|
|
@ -1216,8 +1216,8 @@
|
|||
"customEndpoint": "Connected to {endpoint}",
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search",
|
||||
"autoPairGoogle": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.",
|
||||
"manualPairDesc": "Pair with PIN works for any large screen device you want to play your album on.",
|
||||
"autoPairDesc": "Auto pair works only with devices that support Chromecast.",
|
||||
"manualPairDesc": "Pair with PIN works with any screen you wish to view your album on.",
|
||||
"connectToDevice": "Connect to device",
|
||||
"autoCastDialogBody": "You'll see available Cast devices here.",
|
||||
"autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.",
|
||||
|
|
|
@ -16,7 +16,7 @@ class UpdateService {
|
|||
static final UpdateService instance = UpdateService._privateConstructor();
|
||||
static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
|
||||
static const changeLogVersionKey = "update_change_log_key";
|
||||
static const currentChangeLogVersion = 18;
|
||||
static const currentChangeLogVersion = 19;
|
||||
|
||||
LatestVersionInfo? _latestVersion;
|
||||
final _logger = Logger("UpdateService");
|
||||
|
|
|
@ -31,7 +31,7 @@ class _CastChooseDialogState extends State<CastChooseDialog> {
|
|||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
S.of(context).autoPairGoogle,
|
||||
S.of(context).autoPairDesc,
|
||||
style: textStyle.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import "dart:async";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/update_service.dart';
|
||||
|
@ -9,7 +7,6 @@ import 'package:photos/ui/components/divider_widget.dart';
|
|||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
import 'package:photos/ui/components/title_bar_title_widget.dart';
|
||||
import 'package:photos/ui/notification/update/change_log_entry.dart';
|
||||
import "package:url_launcher/url_launcher_string.dart";
|
||||
|
||||
class ChangeLogPage extends StatefulWidget {
|
||||
const ChangeLogPage({
|
||||
|
@ -81,31 +78,31 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
|
|||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.trailingIconSecondary,
|
||||
buttonSize: ButtonSize.large,
|
||||
labelText: S.of(context).joinDiscord,
|
||||
icon: Icons.discord_outlined,
|
||||
iconColor: enteColorScheme.primary500,
|
||||
onTap: () async {
|
||||
unawaited(
|
||||
launchUrlString(
|
||||
"https://discord.com/invite/z2YVKkycX3",
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// ButtonWidget(
|
||||
// buttonType: ButtonType.trailingIconSecondary,
|
||||
// buttonSize: ButtonSize.large,
|
||||
// labelText: S.of(context).rateTheApp,
|
||||
// icon: Icons.favorite_rounded,
|
||||
// labelText: S.of(context).joinDiscord,
|
||||
// icon: Icons.discord_outlined,
|
||||
// iconColor: enteColorScheme.primary500,
|
||||
// onTap: () async {
|
||||
// await UpdateService.instance.launchReviewUrl();
|
||||
// unawaited(
|
||||
// launchUrlString(
|
||||
// "https://discord.com/invite/z2YVKkycX3",
|
||||
// mode: LaunchMode.externalApplication,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.trailingIconSecondary,
|
||||
buttonSize: ButtonSize.large,
|
||||
labelText: S.of(context).rateTheApp,
|
||||
icon: Icons.favorite_rounded,
|
||||
iconColor: enteColorScheme.primary500,
|
||||
onTap: () async {
|
||||
await UpdateService.instance.launchReviewUrl();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
|
@ -122,18 +119,16 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
|
|||
final List<ChangeLogEntry> items = [];
|
||||
items.addAll([
|
||||
ChangeLogEntry(
|
||||
"Improved Performance for Large Galleries ✨",
|
||||
'We\'ve made significant improvements to how quickly galleries load and'
|
||||
' with less stutter, especially for those with a lot of photos and videos.',
|
||||
"Cast albums to TV ✨",
|
||||
"View a slideshow of your albums on any big screen! Open an album and click on the Cast button to get started.",
|
||||
),
|
||||
ChangeLogEntry(
|
||||
"Enhanced Functionality for Video Backups",
|
||||
'Even if video backups are disabled, you can now manually upload individual videos.',
|
||||
"Own shared photos",
|
||||
"You can now add shared items to your favorites to any of your personal albums. Ente will create a copy that is fully owned by you and can be organized to your liking.",
|
||||
),
|
||||
ChangeLogEntry(
|
||||
"Bug Fixes",
|
||||
'Many a bugs were squashed in this release.\n'
|
||||
'\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏',
|
||||
"Performance improvements",
|
||||
"This release also brings in major changes that should improve responsiveness. If you discover room for improvement, please let us know!",
|
||||
),
|
||||
]);
|
||||
|
||||
|
|
|
@ -376,7 +376,13 @@ class FileUploader {
|
|||
if (Platform.isAndroid) {
|
||||
final bool hasPermission = await Permission.accessMediaLocation.isGranted;
|
||||
if (!hasPermission) {
|
||||
throw NoMediaLocationAccessError();
|
||||
final permissionStatus = await Permission.accessMediaLocation.request();
|
||||
if (!permissionStatus.isGranted) {
|
||||
_logger.severe(
|
||||
"Media location access denied with permission status: ${permissionStatus.name}",
|
||||
);
|
||||
throw NoMediaLocationAccessError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.89+609
|
||||
version: 0.8.90+610
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
|
|
@ -9,8 +9,7 @@ type CastRequest struct {
|
|||
}
|
||||
|
||||
type RegisterDeviceRequest struct {
|
||||
DeviceCode *string `json:"deviceCode"`
|
||||
PublicKey string `json:"publicKey" binding:"required"`
|
||||
PublicKey string `json:"publicKey" binding:"required"`
|
||||
}
|
||||
|
||||
type AuthContext struct {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
entity "github.com/ente-io/museum/ente/cast"
|
||||
"github.com/ente-io/museum/pkg/controller/cast"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
entity "github.com/ente-io/museum/ente/cast"
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/controller/cast"
|
||||
"github.com/ente-io/museum/pkg/utils/handler"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CastHandler exposes request handlers for publicly accessible collections
|
||||
|
@ -126,7 +126,7 @@ func (h *CastHandler) GetDiff(c *gin.Context) {
|
|||
}
|
||||
|
||||
func getDeviceCode(c *gin.Context) string {
|
||||
return c.Param("deviceCode")
|
||||
return strings.ToUpper(c.Param("deviceCode"))
|
||||
}
|
||||
|
||||
func (h *CastHandler) getFileForType(c *gin.Context, objectType ente.ObjectType) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package cast
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/ente/cast"
|
||||
"github.com/ente-io/museum/pkg/controller/access"
|
||||
castRepo "github.com/ente-io/museum/pkg/repo/cast"
|
||||
|
@ -28,7 +27,7 @@ func NewController(castRepo *castRepo.Repository,
|
|||
}
|
||||
|
||||
func (c *Controller) RegisterDevice(ctx *gin.Context, request *cast.RegisterDeviceRequest) (string, error) {
|
||||
return c.CastRepo.AddCode(ctx, request.DeviceCode, request.PublicKey, network.GetClientIP(ctx))
|
||||
return c.CastRepo.AddCode(ctx, request.PublicKey, network.GetClientIP(ctx))
|
||||
}
|
||||
|
||||
func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string, error) {
|
||||
|
@ -42,7 +41,6 @@ func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string,
|
|||
"ip": ip,
|
||||
"clientIP": network.GetClientIP(ctx),
|
||||
}).Warn("GetPublicKey: IP mismatch")
|
||||
return "", &ente.ErrCastIPMismatch
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package storagebonus
|
|||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/utils/random"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
entity "github.com/ente-io/museum/ente/storagebonus"
|
||||
|
@ -119,7 +119,7 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s
|
|||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, stacktrace.Propagate(err, "failed to get storagebonus code")
|
||||
}
|
||||
code, err := generateAlphaNumString(codeLength)
|
||||
code, err := random.GenerateAlphaNumString(codeLength)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
@ -131,30 +131,3 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s
|
|||
}
|
||||
return referralCode, nil
|
||||
}
|
||||
|
||||
// generateAlphaNumString returns AlphaNumeric code of given length
|
||||
// which exclude number 0 and letter O. The code always starts with an
|
||||
// alphabet
|
||||
func generateAlphaNumString(length int) (string, error) {
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphaNum := fmt.Sprintf("%s123456789", alphabet)
|
||||
// Allocate a byte slice with the desired length.
|
||||
result := make([]byte, length)
|
||||
// Generate the first letter as an alphabet.
|
||||
r0, err := auth.GenerateRandomInt(int64(len(alphabet)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[0] = alphabet[r0]
|
||||
// Generate the remaining characters as alphanumeric.
|
||||
for i := 1; i < length; i++ {
|
||||
ri, err := auth.GenerateRandomInt(int64(len(alphaNum)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[i] = alphaNum[ri]
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
|
|
@ -8,23 +8,16 @@ import (
|
|||
"github.com/ente-io/stacktrace"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (r *Repository) AddCode(ctx context.Context, code *string, pubKey string, ip string) (string, error) {
|
||||
var codeValue string
|
||||
var err error
|
||||
if code == nil || *code == "" {
|
||||
codeValue, err = random.GenerateSixDigitOtp()
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
} else {
|
||||
codeValue = strings.TrimSpace(*code)
|
||||
func (r *Repository) AddCode(ctx context.Context, pubKey string, ip string) (string, error) {
|
||||
codeValue, err := random.GenerateAlphaNumString(6)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = r.DB.ExecContext(ctx, "INSERT INTO casting (code, public_key, id, ip) VALUES ($1, $2, $3, $4)", codeValue, pubKey, uuid.New(), ip)
|
||||
if err != nil {
|
||||
|
|
|
@ -13,3 +13,30 @@ func GenerateSixDigitOtp() (string, error) {
|
|||
}
|
||||
return fmt.Sprintf("%06d", n), nil
|
||||
}
|
||||
|
||||
// GenerateAlphaNumString returns AlphaNumeric code of given length
|
||||
// which exclude number 0 and letter O. The code always starts with an
|
||||
// alphabet
|
||||
func GenerateAlphaNumString(length int) (string, error) {
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphaNum := fmt.Sprintf("%s123456789", alphabet)
|
||||
// Allocate a byte slice with the desired length.
|
||||
result := make([]byte, length)
|
||||
// Generate the first letter as an alphabet.
|
||||
r0, err := auth.GenerateRandomInt(int64(len(alphabet)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[0] = alphabet[r0]
|
||||
// Generate the remaining characters as alphanumeric.
|
||||
for i := 1; i < length; i++ {
|
||||
ri, err := auth.GenerateRandomInt(int64(len(alphaNum)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[i] = alphaNum[ri]
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
|
|
@ -161,9 +161,7 @@ export default function AlbumCastDialog(props: Props) {
|
|||
{browserCanCast && (
|
||||
<>
|
||||
<Typography color={"text.muted"}>
|
||||
{t(
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE",
|
||||
)}
|
||||
{t("AUTO_CAST_PAIR_DESC")}
|
||||
</Typography>
|
||||
|
||||
<EnteButton
|
||||
|
@ -179,7 +177,7 @@ export default function AlbumCastDialog(props: Props) {
|
|||
</>
|
||||
)}
|
||||
<Typography color="text.muted">
|
||||
{t("PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE")}
|
||||
{t("PAIR_WITH_PIN_DESC")}
|
||||
</Typography>
|
||||
|
||||
<EnteButton
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "Geräte koppeln",
|
||||
"TV_NOT_FOUND": "Fernseher nicht gefunden. Hast du die PIN korrekt eingegeben?",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "Freihand",
|
||||
|
|
|
@ -598,13 +598,13 @@
|
|||
"ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.",
|
||||
"PAIR_DEVICE_TO_TV": "Pair devices",
|
||||
"TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?",
|
||||
"AUTO_CAST_PAIR": "Auto Pair",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.",
|
||||
"AUTO_CAST_PAIR": "Auto pair",
|
||||
"AUTO_CAST_PAIR_DESC": "Auto pair works only with devices that support Chromecast.",
|
||||
"PAIR_WITH_PIN": "Pair with PIN",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.",
|
||||
"PAIR_WITH_PIN_DESC": "Pair with PIN works with any screen you wish to view your album on.",
|
||||
"VISIT_CAST_ENTE_IO": "Visit <a>{{url}}</a> on the device you want to pair.",
|
||||
"CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.",
|
||||
"CAST_AUTO_PAIR_FAILED": "Chromecast auto pair failed. Please try again.",
|
||||
"FREEHAND": "Freehand",
|
||||
"APPLY_CROP": "Apply Crop",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving.",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -598,13 +598,13 @@
|
|||
"ENTER_CAST_PIN_CODE": "Entrez le code que vous voyez sur la TV ci-dessous pour appairer cet appareil.",
|
||||
"PAIR_DEVICE_TO_TV": "Associer les appareils",
|
||||
"TV_NOT_FOUND": "TV introuvable. Avez-vous entré le code PIN correctement ?",
|
||||
"AUTO_CAST_PAIR": "Paire automatique",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "La paire automatique nécessite la connexion aux serveurs Google et ne fonctionne qu'avec les appareils pris en charge par Chromecast. Google ne recevra pas de données sensibles, telles que vos photos.",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "Associer avec le code PIN",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "Choisissez un périphérique compatible avec la caste à partir de la fenêtre pop-up du navigateur.",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "L'association avec le code PIN fonctionne pour tout appareil grand écran sur lequel vous voulez lire votre album.",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "Visitez <a>{{url}}</a> sur l'appareil que vous voulez associer.",
|
||||
"CAST_AUTO_PAIR_FAILED": "La paire automatique de Chromecast a échoué. Veuillez réessayer.",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "Main levée",
|
||||
"APPLY_CROP": "Appliquer le recadrage",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder.",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -598,13 +598,13 @@
|
|||
"ENTER_CAST_PIN_CODE": "Voer de code in die u op de TV ziet om dit apparaat te koppelen.",
|
||||
"PAIR_DEVICE_TO_TV": "Koppel apparaten",
|
||||
"TV_NOT_FOUND": "TV niet gevonden. Heeft u de pincode correct ingevoerd?",
|
||||
"AUTO_CAST_PAIR": "Automatisch koppelen",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Automatisch koppelen vereist verbinding met Google-servers en werkt alleen met apparaten die door Chromecast worden ondersteund. Google zal geen gevoelige gegevens ontvangen, zoals uw foto's.",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "Koppelen met PIN",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "Kies een compatibel apparaat uit de browser popup.",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Koppelen met PIN werkt op elk groot schermapparaat waarop u uw album wilt afspelen.",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "Bezoek <a>{{url}}</a> op het apparaat dat je wilt koppelen.",
|
||||
"CAST_AUTO_PAIR_FAILED": "Auto koppelen van Chromecast is mislukt. Probeer het opnieuw.",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "Losse hand",
|
||||
"APPLY_CROP": "Bijsnijden toepassen",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.",
|
||||
|
|
|
@ -598,13 +598,13 @@
|
|||
"ENTER_CAST_PIN_CODE": "Digite o código que você vê na TV abaixo para parear este dispositivo.",
|
||||
"PAIR_DEVICE_TO_TV": "Parear dispositivos",
|
||||
"TV_NOT_FOUND": "TV não encontrada. Você inseriu o PIN correto?",
|
||||
"AUTO_CAST_PAIR": "Pareamento automático",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "O Auto Pair requer a conexão com servidores do Google e só funciona com dispositivos Chromecast. O Google não receberá dados confidenciais, como suas fotos.",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "Parear com PIN",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "Escolha um dispositivo compatível com casts no navegador popup.",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Parear com o PIN funciona para qualquer dispositivo de tela grande onde você deseja reproduzir seu álbum.",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "Acesse <a>{{url}}</a> no dispositivo que você deseja parear.",
|
||||
"CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair falhou. Por favor, tente novamente.",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "Mão livre",
|
||||
"APPLY_CROP": "Aplicar Recorte",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -598,13 +598,13 @@
|
|||
"ENTER_CAST_PIN_CODE": "Введите код, который вы видите на экране телевизора ниже, чтобы выполнить сопряжение с этим устройством.",
|
||||
"PAIR_DEVICE_TO_TV": "Сопряжение устройств",
|
||||
"TV_NOT_FOUND": "Телевизор не найден. Вы правильно ввели PIN-код?",
|
||||
"AUTO_CAST_PAIR": "Автоматическое сопряжение",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Автоматическое сопряжение требует подключения к серверам Google и работает только с устройствами, поддерживающими Chromecast. Google не будет получать конфиденциальные данные, такие как ваши фотографии.",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "Соединение с помощью булавки",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "Выберите устройство, совместимое с cast, во всплывающем окне браузера.",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Сопряжение с помощью PIN-кода работает на любом устройстве с большим экраном, на котором вы хотите воспроизвести свой альбом.",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "Перейдите на страницу <a>{{url}}</a> на устройстве, которое вы хотите подключить.",
|
||||
"CAST_AUTO_PAIR_FAILED": "Не удалось выполнить автоматическое сопряжение Chromecast. Пожалуйста, попробуйте снова.",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "От руки",
|
||||
"APPLY_CROP": "Применить обрезку",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "Перед сохранением необходимо выполнить по крайней мере одно преобразование или корректировку цвета.",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -599,10 +599,10 @@
|
|||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
|
|
|
@ -598,13 +598,13 @@
|
|||
"ENTER_CAST_PIN_CODE": "输入您在下面的电视上看到的代码来配对此设备。",
|
||||
"PAIR_DEVICE_TO_TV": "配对设备",
|
||||
"TV_NOT_FOUND": "未找到电视。您输入的 PIN 码正确吗?",
|
||||
"AUTO_CAST_PAIR": "自动配对",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "自动配对需要连接到 Google 服务器,且仅适用于支持 Chromecast 的设备。Google 不会接收敏感数据,例如您的照片。",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_DESC": "",
|
||||
"PAIR_WITH_PIN": "用 PIN 配对",
|
||||
"CHOOSE_DEVICE_FROM_BROWSER": "从浏览器弹出窗口中选择兼容 Cast 的设备。",
|
||||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "用 PIN 配对适用于任何大屏幕设备,您可以在这些设备上播放您的相册。",
|
||||
"PAIR_WITH_PIN_DESC": "",
|
||||
"VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 <a>{{url}}</a> 。",
|
||||
"CAST_AUTO_PAIR_FAILED": "Chromecast 自动配对失败。请再试一次。",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "手画",
|
||||
"APPLY_CROP": "应用裁剪",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。",
|
||||
|
|
Loading…
Add table
Reference in a new issue