add new workflow for additional users

This commit is contained in:
otsmr 2026-01-14 22:32:04 +01:00
parent d2b881e281
commit 36e309d58a
23 changed files with 2452 additions and 1563 deletions

4
.gitmodules vendored
View file

@ -2,3 +2,7 @@
[submodule "dependencies"]
path = dependencies
url = https://github.com/twonlyapp/twonly-app-dependencies.git
[submodule "lib/src/localization/translations"]
path = lib/src/localization/translations
url = ssh://git@git.twonly.eu:22222/twonly/twonly-translations.git
# https://git.twonly.eu/twonly/twonly-translations

View file

@ -1,5 +1,5 @@
arb-dir: lib/src/localization
template-arb-file: app_en.arb
arb-dir: lib/src/localization/translations
template-arb-file: en.arb
output-localization-file: app_localizations.dart
untranslated-messages-file: build/l10n.log
output-dir: lib/src/localization/generated

View file

@ -1,468 +0,0 @@
{
"@@locale": "de",
"registerTitle": "Willkommen bei twonly!",
"registerSlogan": "twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.",
"onboardingWelcomeTitle": "Willkommen bei twonly!",
"onboardingWelcomeBody": "Erlebe eine private und sichere Möglichkeit mit Freunden in Kontakt zu bleiben, indem du spontane Bilder teilst.",
"onboardingE2eTitle": "Unbekümmert teilen",
"onboardingE2eBody": "Genieße durch die Ende-zu-Ende-Verschlüsselung die Gewissheit, dass nur du und deine Freunde die geteilten Momente sehen können.",
"onboardingFocusTitle": "Fokussiere dich auf das Teilen von Momenten",
"onboardingFocusBody": "Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.",
"onboardingSendTwonliesTitle": "twonlies senden",
"onboardingSendTwonliesBody": "Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!",
"onboardingNotProductTitle": "Du bist nicht das Produkt!",
"onboardingNotProductBody": "twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.",
"onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei",
"onboardingBuyOneGetTwoBody": "twonly benötigt immer mindestens zwei Personen, daher erhältst du beim Kauf eine zweite kostenlose Lizenz für deinen twonly-Partner.",
"onboardingGetStartedTitle": "Auf geht's",
"onboardingGetStartedBody": "Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.",
"onboardingTryForFree": "Jetzt registrieren",
"registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!",
"registerUsernameDecoration": "Benutzername",
"registerUsernameLimits": "Der Benutzername muss mindestens 3 Zeichen lang sein.",
"registerProofOfWorkFailed": "Beim Captcha-Test gab es ein Problem. Bitte versuche es erneut.",
"registerSubmitButton": "Jetzt registrieren!",
"registerTwonlyCodeText": "Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!",
"registerTwonlyCodeLabel": "twonly-Code",
"newMessageTitle": "Neue Nachricht",
"chatsTapToSend": "Klicke, um dein erstes Bild zu teilen.",
"cameraPreviewSendTo": "Senden an",
"shareImageTitle": "Teilen mit",
"shareImageBestFriends": "Beste Freunde",
"shareImagePinnedContacts": "Angeheftet",
"shareImagedEditorSendImage": "Senden",
"shareImagedEditorShareWith": "Teilen mit",
"shareImagedEditorSaveImage": "Speichern",
"shareImagedEditorSavedImage": "Gespeichert",
"shareImagedSelectAll": "Alle auswählen",
"shareImageAllUsers": "Alle Kontakte",
"shareImageAllTwonlyWarning": "twonlies können nur an verifizierte Kontakte gesendet werden!",
"shareImageSearchAllContacts": "Alle Kontakte durchsuchen",
"shareImageUserNotVerified": "Benutzer ist nicht verifiziert",
"shareImageUserNotVerifiedDesc": "twonlies können nur an verifizierte Nutzer gesendet werden. Um einen Nutzer zu verifizieren, gehe auf sein Profil und auf „Sicherheitsnummer verifizieren“.",
"shareImageShowArchived": "Archivierte Benutzer anzeigen",
"startNewChatSearchHint": "Name, Benutzername oder Gruppenname",
"searchUsernameInput": "Benutzername",
"searchUsernameTitle": "Benutzernamen suchen",
"searchUserNamePreview": "Um dich und andere twonly Benutzer vor Spam und Missbrauch zu schützen, ist es nicht möglich, im Preview-Modus nach anderen Personen zu suchen. Andere Benutzer können dich finden und deren Anfragen werden dann hier angezeigt!",
"selectSubscription": "Abo auswählen",
"searchUsernameNotFound": "Benutzername nicht gefunden",
"searchUsernameNotFoundBody": "Es wurde kein Benutzer mit dem Benutzernamen \"{username}\" gefunden.",
"searchUsernameNewFollowerTitle": "Folgeanfragen",
"searchUsernameQrCodeBtn": "QR-Code scannen",
"searchUserNamePending": "Ausstehend",
"searchUserNameBlockUserTooltip": "Benutzer ohne Benachrichtigung blockieren.",
"searchUserNameRejectUserTooltip": "Die Anfrage ablehnen und den Anfragenden informieren.",
"searchUserNameArchiveUserTooltip": "Benutzer archivieren. Du wirst informiert sobald er deine Anfrage akzeptiert.",
"userFound": "{username} gefunden",
"userFoundBody": "Möchtest du eine Folgeanfrage stellen?",
"chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!",
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
"chatListDetailInput": "Nachricht eingeben",
"userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.",
"contextMenuUserProfile": "Userprofil",
"contextMenuVerifyUser": "Verifizieren",
"contextMenuArchiveUser": "Archivieren",
"contextMenuUndoArchiveUser": "Archivierung aufheben",
"startNewChatTitle": "Kontakt wählen",
"startNewChatNewContact": "Neuer Kontakt",
"startNewChatYourContacts": "Deine Kontakte",
"contextMenuOpenChat": "Chat",
"contextMenuPin": "Anheften",
"contextMenuUnpin": "Lösen",
"mediaViewerAuthReason": "Bitte authentifiziere dich, um diesen twonly zu sehen!",
"mediaViewerTwonlyTapToOpen": "Tippe um den twonly zu öffnen!",
"messageSendState_Received": "Empfangen",
"messageSendState_Opened": "Geöffnet",
"messageSendState_Send": "Gesendet",
"messageSendState_Sending": "Wird gesendet",
"messageSendState_TapToLoad": "Tippe zum Laden",
"messageSendState_Loading": "Herunterladen",
"messageStoredInGallery": "Gespeichert",
"messageReopened": "Erneut geöffnet",
"imageEditorDrawOk": "Zeichnung machen",
"settingsTitle": "Einstellungen",
"settingsChats": "Chats",
"settingsStorageData": "Daten und Speicher",
"settingsStorageDataStoreInGTitle": "In der Galerie speichern",
"settingsStorageDataStoreInGSubtitle": "Speichere Bilder zusätzlich in der Systemgalerie.",
"settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload",
"settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten",
"settingsStorageDataAutoDownWifi": "Bei Nutzung von WLAN",
"settingsPreSelectedReactions": "Vorgewählte Reaktions-Emojis",
"settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.",
"settingsProfile": "Profil",
"settingsProfileCustomizeAvatar": "Avatar anpassen",
"settingsProfileEditDisplayName": "Anzeigename",
"settingsProfileEditDisplayNameNew": "Neuer Anzeigename",
"settingsAccount": "Konto",
"settingsSubscription": "Abonnement",
"settingsAppearance": "Erscheinungsbild",
"settingsPrivacy": "Datenschutz",
"settingsPrivacyBlockUsers": "Benutzer blockieren",
"settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.",
"settingsPrivacyBlockUsersCount": "{len} Kontakt(e)",
"settingsNotification": "Benachrichtigung",
"settingsNotifyTroubleshooting": "Fehlersuche",
"settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.",
"settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt",
"settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.",
"settingsHelp": "Hilfe",
"settingsHelpFAQ": "FAQ",
"feedbackTooltip": "Feedback zur Verbesserung von twonly geben.",
"settingsHelpContactUs": "Kontaktiere uns",
"contactUsFaq": "FAQ schon gelesen?",
"contactUsEmojis": "Wie fühlst du dich? (optional)",
"contactUsSelectOption": "Bitte wähle eine Option",
"contactUsReason": "Sag uns, warum du uns kontaktierst",
"contactUsMessage": "Wenn du eine Antwort erhalten möchtest, füge bitte deine E-Mail-Adresse hinzu, damit wir dich kontaktieren können.",
"contactUsYourMessage": "Deine Nachricht",
"contactUsMessageTitle": "Erzähl uns, was los ist",
"contactUsReasonNotWorking": "Etwas funktioniert nicht",
"contactUsReasonFeatureRequest": "Funktionsanfrage",
"contactUsReasonQuestion": "Frage",
"contactUsReasonFeedback": "Feedback",
"contactUsReasonOther": "Sonstiges",
"contactUsIncludeLog": "Debug-Protokoll anhängen.",
"contactUsWhatsThat": "Was ist das?",
"contactUsLastWarning": "Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.",
"contactUsSuccess": "Feedback erfolgreich übermittelt!",
"contactUsShortcut": "Feedback-Symbol ausblenden",
"settingsHelpDiagnostics": "Diagnoseprotokoll",
"settingsHelpVersion": "Version",
"settingsHelpLicenses": "Lizenzen (Source-Code)",
"settingsHelpCredits": "Lizenzen (Bilder)",
"settingsHelpImprint": "Impressum & Datenschutzrichtlinie",
"settingsHelpTerms": "Nutzungsbedingungen",
"settingsAppearanceTheme": "Theme",
"settingsAccountDeleteAccount": "Konto löschen",
"settingsAccountDeleteAccountWithBallance": "Im nächsten Schritt kannst du auswählen, was du mit dem Restguthaben ({credit}) machen willst.",
"settingsAccountDeleteAccountNoInternet": "Zum Löschen deines Accounts ist eine Internetverbindung erforderlich.",
"settingsAccountDeleteAccountNoBallance": "Wenn du dein Konto gelöscht hast, gibt es keinen Weg zurück.",
"settingsAccountDeleteModalTitle": "Bist du sicher?",
"settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.",
"contactVerifyNumberTitle": "Sicherheitsnummer verifizieren",
"contactVerifyNumberTapToScan": "Zum Scannen tippen",
"contactVerifyNumberMarkAsVerified": "Als verifiziert markieren",
"contactVerifyNumberClearVerification": "Verifizierung aufheben",
"contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.",
"contactNickname": "Spitzname",
"contactNicknameNew": "Neuer Spitzname",
"contactBlock": "Blockieren",
"contactRemove": "Benutzer löschen",
"contactRemoveTitle": "{username} löschen?",
"contactRemoveBody": "Entferne den Benutzer und lösche den Chat sowie alle zugehörigen Mediendateien dauerhaft. Dadurch wird auch DEIN KONTO VON DEM TELEFON DEINES KONTAKTS gelöscht.",
"deleteAllContactMessages": "Textnachrichten löschen",
"deleteAllContactMessagesBody": "Dadurch werden alle Nachrichten, ausgenommen gespeicherte Mediendateien, in deinem Chat mit {username} gelöscht. Dies löscht NICHT die auf dem Gerät von {username} gespeicherten Nachrichten!",
"contactBlockTitle": "Blockiere {username}",
"contactBlockBody": "Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und sein Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.",
"undo": "Rückgängig",
"redo": "Wiederholen",
"next": "Weiter",
"submit": "Abschicken",
"close": "Schließen",
"cancel": "Abbrechen",
"edit": "Bearbeiten",
"ok": "Ok",
"now": "Jetzt",
"you": "Du",
"minutesShort": "Min.",
"image": "Bild",
"video": "Video",
"react": "Reagieren",
"reply": "Antworten",
"copy": "Kopieren",
"delete": "Löschen",
"info": "Info",
"disable": "Deaktiviern",
"enable": "Aktivieren",
"switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.",
"addTextItem": "Text",
"protectAsARealTwonly": "Als echtes twonly senden!",
"addDrawing": "Zeichnung",
"addEmoji": "Emoji",
"toggleFlashLight": "Taschenlampe umschalten",
"toggleHighQuality": "Bessere Auflösung umschalten",
"searchUsernameNotFoundLong": "\"{username}\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.",
"errorUnknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.",
"errorBadRequest": "Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.",
"errorTooManyRequests": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.",
"errorInternalError": "Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.",
"errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.",
"errorUsernameAlreadyTaken": "Der Benutzername ist bereits vergeben.",
"errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.",
"errorUsernameNotFound": "Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.",
"errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.",
"errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.",
"errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.",
"errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.",
"errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.",
"upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.",
"upgradeToPaidPlanButton": "Auf {planId} upgraden{sufix}",
"partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!",
"errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.",
"errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.",
"errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.",
"errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.",
"errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.",
"proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"proFeature2": "✓ 1 zusätzlicher Plus Benutzer",
"proFeature3": "✓ Flammen wiederherstellen",
"proFeature4": "✓ twonly unterstützen",
"year": "Jahr",
"month": "Monat",
"yearly": "Jährlich",
"monthly": "Monatlich",
"familyFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"familyFeature2": "✓ 4 zusätzliche Plus Benutzer",
"familyFeature3": "✓ Flammen wiederherstellen",
"familyFeature4": "✓ twonly unterstützen",
"redeemUserInviteCode": "Oder löse einen twonly-Code ein.",
"freeFeature1": "✓ 10 Medien-Datei-Uploads pro Tag",
"plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"plusFeature2": "✓ Zusatzfunktionen (coming-soon)",
"transactionHistory": "Transaktionshistorie",
"currentBalance": "Dein Guthaben",
"manageAdditionalUsers": "Zusätzliche Benutzer verwalten",
"manageSubscription": "Abonnement verwalten",
"nextPayment": "Nächste Zahlung",
"open": "Offene",
"buy": "Kaufen",
"createOrRedeemVoucher": "Gutschein erstellen oder einlösen",
"subscriptionRefund": "Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von {refund} für dein aktuelles Abonnement.",
"createVoucher": "Gutschein kaufen",
"createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.",
"redeemVoucher": "Gutschein einlösen",
"redeemUserInviteCodeTitle": "twonly-Code einlösen",
"redeemUserInviteCodeSuccess": "Dein Plan wurde erfolgreich angepasst.",
"voucherCreated": "Gutschein wurde erstellt",
"openVouchers": "Offene Gutscheine",
"enterVoucherCode": "Gutschein Code eingeben",
"voucherRedeemed": "Gutschein eingelöst",
"requestedVouchers": "Beantragte Gutscheine",
"redeemedVouchers": "Eingelöste Gutscheine",
"transactionCash": "Bargeldtransaktion",
"transactionPlanUpgrade": "Planupgrade",
"transactionRefund": "Rückerstattung",
"transactionAutoRenewal": "Automatische Verlängerung",
"refund": "Rückerstattung",
"transactionThanksForTesting": "Danke fürs Testen",
"transactionUnknown": "Unbekannte Transaktion",
"transactionVoucherCreated": "Gutschein erstellt",
"transactionVoucherRedeemed": "Gutschein eingelöst",
"checkoutOptions": "Optionen",
"checkoutPayYearly": "Jährlich bezahlen",
"checkoutTotal": "Gesamt",
"selectPaymentMethod": "Zahlungsmethode auswählen",
"twonlyCredit": "twonly-Guthaben",
"notEnoughCredit": "Du hast nicht genügend Guthaben!",
"chargeCredit": "Guthaben aufladen",
"autoRenewal": "Automatische Verlängerung",
"autoRenewalDesc": "Du kannst dies jederzeit ändern.",
"autoRenewalLongDesc": "Wenn dein Abonnement ausläuft, wirst du automatisch auf den Preview-Plan zurückgestuft. Wenn du die automatische Verlängerung aktivierst, vergewissere dich bitte, dass du über genügend Guthaben für die automatische Erneuerung verfügst. Wir werden dich rechtzeitig vor der automatischen Erneuerung benachrichtigen.",
"planSuccessUpgraded": "Dein Plan wurde erfolgreich aktualisiert.",
"checkoutSubmit": "Kostenpflichtig bestellen",
"additionalUsersList": "Ihre zusätzlichen Benutzer",
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer",
"additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer",
"planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
"planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
"galleryDelete": "Datei löschen",
"galleryExport": "In Galerie exportieren",
"galleryExportSuccess": "Erfolgreich in der Gallery gespeichert.",
"galleryDetails": "Details anzeigen",
"settingsResetTutorials": "Tutorials erneut anzeigen",
"settingsResetTutorialsDesc": "Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.",
"settingsResetTutorialsSuccess": "Tutorials werden erneut angezeigt.",
"tutorialChatListSearchUsersTitle": "Freunde finden und Freundschaftsanfragen verwalten",
"tutorialChatListSearchUsersDesc": "Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.",
"tutorialChatListContextMenuTitle": "Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.",
"tutorialChatListContextMenuDesc": "Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.",
"tutorialChatMessagesVerifyShieldTitle": "Verifiziere deine Kontakte!",
"tutorialChatMessagesVerifyShieldDesc": "twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.",
"tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen",
"tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.",
"memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.",
"deleteTitle": "Bist du dir sicher?",
"deleteOkBtnForAll": "Für alle löschen",
"deleteOkBtnForMe": "Für mich löschen",
"deleteImageTitle": "Bist du dir sicher?",
"deleteImageBody": "Das Bild wird unwiderruflich gelöscht.",
"backupNoticeTitle": "Kein Backup konfiguriert",
"backupNoticeDesc": "Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.",
"backupNoticeLater": "Später erinnern",
"backupNoticeOpenBackup": "Backup erstellen",
"backupPending": "Ausstehend",
"backupFailed": "Fehlgeschlagen",
"backupSuccess": "Erfolgreich",
"backupTwonlySafeDesc": "Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.",
"backupServer": "Server",
"backupMaxBackupSize": "max. Backup-Größe",
"backupStorageRetention": "Speicheraufbewahrung",
"backupLastBackupDate": "Letztes Backup",
"backupLastBackupSize": "Backup-Größe",
"backupLastBackupResult": "Ergebnis",
"deleteBackupTitle": "Bist du sicher?",
"backupNoPasswordRecovery": "Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.",
"deleteBackupBody": "Ohne ein Backup kannst du dein Benutzerkonto nicht wiederherstellen.",
"backupData": "Daten-Backup",
"backupDataDesc": "Das Daten-Backup enthält neben deiner twonly-Identität auch alle deine Mediendateien. Dieses Backup ist ebenfalls verschlüsselt, wird jedoch lokal gespeichert. Du musst es dann manuell auf deinen Laptop oder ein Gerät deiner Wahl kopieren.",
"backupInsecurePassword": "Unsicheres Passwort",
"backupInsecurePasswordDesc": "Das gewählte Passwort ist sehr unsicher und kann daher leicht von Angreifern erraten werden. Bitte wähle ein sicheres Passwort.",
"backupInsecurePasswordOk": "Trotzdem fortfahren",
"backupInsecurePasswordCancel": "Erneut versuchen",
"backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.",
"backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.",
"password": "Passwort",
"passwordRepeated": "Passwort wiederholen",
"passwordRepeatedNotEqual": "Passwörter stimmen nicht überein.",
"backupPasswordRequirement": "Das Passwort muss mindestens 8 Zeichen lang sein.",
"backupExpertSettings": "Experteneinstellungen",
"backupEnableBackup": "Automatische Sicherung aktivieren",
"backupOwnServerDesc": "Speichere dein twonly Backup auf einem Server deiner Wahl.",
"backupUseOwnServer": "Server verwenden",
"backupResetServer": "Standardserver verwenden",
"backupTwonlySaveNow": "Jetzt speichern",
"backupChangePassword": "Password ändern",
"inviteFriends": "Freunde einladen",
"inviteFriendsShareBtn": "Teilen",
"inviteFriendsShareText": "Wechseln wir zu twonly: {url}",
"appOutdated": "Deine Version von twonly ist veraltet.",
"appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"uploadLimitReached": "Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.",
"retransmissionRequested": "Wird erneut versucht.",
"testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!",
"openChangeLog": "Changelog automatisch öffnen",
"reportUserTitle": "Melde {username}",
"reportUserReason": "Meldegrund",
"reportUser": "Benutzer melden",
"newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.",
"tabToRemoveEmoji": "Tippen um zu entfernen",
"quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.",
"messageWasDeleted": "Nachricht wurde gelöscht.",
"messageWasDeletedShort": "Gelöscht",
"sent": "Versendet",
"sentTo": "Zugestellt an",
"received": "Empfangen",
"opened": "Geöffnet",
"waitingForInternet": "Warten auf Internet",
"editHistory": "Bearbeitungshistorie",
"archivedChats": "Archivierte Chats",
"durationShortSecond": "Sek.",
"durationShortMinute": "Min.",
"durationShortHour": "Std.",
"durationShortDays": "{count, plural, =1{1 Tag} other{{count} Tage}}",
"contacts": "Kontakte",
"groups": "Gruppen",
"newGroup": "Neue Gruppe",
"selectMembers": "Mitglieder auswählen",
"selectGroupName": "Gruppennamen wählen",
"groupNameInput": "Gruppennamen",
"groupMembers": "Mitglieder",
"createGroup": "Gruppe erstellen",
"addMember": "Mitglied hinzufügen",
"leaveGroup": "Gruppe verlassen",
"createContactRequest": "Kontaktanfrage erstellen",
"contactRequestSend": "Kontakanfrage gesendet",
"makeAdmin": "Zum Admin machen",
"removeAdmin": "Als Admin entfernen",
"removeFromGroup": "Aus Gruppe entfernen",
"admin": "Admin",
"revokeAdminRightsTitle": "Adminrechte von {username} entfernen?",
"revokeAdminRightsOkBtn": "Als Admin entfernen",
"makeAdminRightsTitle": "{username} zum Admin machen?",
"makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.",
"makeAdminRightsOkBtn": "Zum Admin machen",
"updateGroup": "Gruppe aktualisieren",
"alreadyInGroup": "Bereits Mitglied",
"removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?",
"youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.",
"makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.",
"youCreatedGroup": "Du hast die Gruppe erstellt.",
"makerCreatedGroup": "{maker} hat die Gruppe erstellt.",
"youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.",
"makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.",
"youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.",
"makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.",
"youMadeAdmin": "Du hast {affected} zum Administrator gemacht.",
"makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.",
"youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.",
"makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.",
"youLeftGroup": "Du hast die Gruppe verlassen.",
"makerLeftGroup": "{maker} hat die Gruppe verlassen.",
"groupActionYou": "dich",
"groupActionYour": "deine",
"settingsBackup": "Backup",
"twonlySafeRecoverTitle": "Recovery",
"twonlySafeRecoverDesc": "Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.",
"twonlySafeRecoverBtn": "Backup wiederherstellen",
"notificationFillerIn": "in",
"notificationText": "hat eine Nachricht{inGroup} gesendet.",
"notificationTwonly": "hat ein twonly{inGroup} gesendet.",
"notificationVideo": "hat ein Video{inGroup} gesendet.",
"notificationImage": "hat ein Bild{inGroup} gesendet.",
"notificationAudio": "hat eine Sprachnachricht{inGroup} gesendet.",
"notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.",
"notificationContactRequest": "möchte sich mit dir vernetzen.",
"notificationAcceptRequest": "ist jetzt mit dir vernetzt.",
"notificationStoredMediaFile": "hat dein Bild gespeichert.",
"notificationReaction": "hat auf dein Bild reagiert.",
"notificationReopenedMedia": "hat dein Bild erneut geöffnet.",
"notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.",
"notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.",
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
"notificationResponse": "hat dir{inGroup} geantwortet.",
"notificationTitleUnknownUser": "[Unbekannt]",
"notificationCategoryMessageTitle": "Nachrichten",
"notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.",
"groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.",
"groupYouAreNowLongerAMember": "Du bist nicht mehr Mitglied dieser Gruppe.",
"groupNetworkIssue": "Netzwerkproblem. Bitte probiere es später noch einmal.",
"leaveGroupSelectOtherAdminTitle": "Einen Admin auswählen",
"leaveGroupSelectOtherAdminBody": "Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.",
"leaveGroupSureTitle": "Gruppe verlassen",
"leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?",
"leaveGroupSureOkBtn": "Gruppe verlassen",
"changeDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht ({username}).",
"youChangedDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht.",
"userGotReported": "Benutzer wurde gemeldet.",
"deleteChatAfter": "Chat löschen nach...",
"deleteChatAfterAnHour": "einer Stunde.",
"deleteChatAfterADay": "einem Tag.",
"deleteChatAfterAWeek": "einer Woche.",
"deleteChatAfterAMonth": "einem Monat.",
"deleteChatAfterAYear": "einem Jahr.",
"yourTwonlyScore": "Dein twonly-Score",
"registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.",
"dialogAskDeleteMediaFilePopTitle": "Bist du sicher, dass du dein Meisterwerk löschen möchtest?",
"dialogAskDeleteMediaFilePopDelete": "Löschen",
"allowErrorTracking": "Fehler und Crashes mit uns teilen",
"allowErrorTrackingSubtitle": "Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen.",
"avatarSaveChanges": "Möchtest du die Änderungen speichern?",
"avatarSaveChangesStore": "Speichern",
"avatarSaveChangesDiscard": "Verwerfen",
"inProcess": "Wird verarbeitet",
"draftMessage": "Entwurf",
"exportMemories": "Memories exportieren (Beta)",
"importMemories": "Memories importieren (Beta)",
"voiceMessageSlideToCancel": "Zum Abbrechen ziehen",
"voiceMessageCancel": "Abbrechen",
"shareYourProfile": "Teile dein Profil",
"scanOtherProfile": "Scanne ein anderes Profil",
"skipForNow": "Vorerst überspringen",
"linkFromUsername": "Ist der Link von {username}?",
"linkFromUsernameLong": "Wenn du den Link von der Person direkt erhalten hast, kannst du den Kontakt als verifiziert markieren, da der öffentliche Schlüssel im Link mit dem bereits für diesen Benutzer gespeicherten öffentlichen Schlüssel übereinstimmt.",
"gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.",
"couldNotVerifyUsername": "{username} konnte nicht verifiziert werden",
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!",
"startWithCameraOpen": "Mit geöffneter Kamera starten",
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen",
"verifiedPublicKey": "Der öffentliche Schlüssel von {username} wurde überprüft und ist gültig.",
"memoriesAYearAgo": "Vor einem Jahr",
"memoriesXYearsAgo": "Vor {years} Jahren",
"migrationOfMemories": "Migration von Mediendateien: {open} noch offen.",
"autoStoreAllSendUnlimitedMediaFiles": "Alle gesendeten Medien speichern",
"autoStoreAllSendUnlimitedMediaFilesSubtitle": "Wenn du diese Option aktivierst, werden alle Bilder, die du sendest, gespeichert, sofern sie mit einem unendlichen Countdown und nicht im twonly-Modus gesendet wurden."
}

View file

@ -1,498 +0,0 @@
{
"@@locale": "en",
"registerTitle": "Welcome to twonly!",
"registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing",
"onboardingWelcomeTitle": "Welcome to twonly!",
"onboardingWelcomeBody": "Experience a private and secure way to stay in touch with friends by sharing instant pictures.",
"onboardingE2eTitle": "Carefree sharing",
"onboardingE2eBody": "With end-to-end encryption, enjoy the peace of mind that only you and your friends can see the moments you share.",
"onboardingFocusTitle": "Focus on sharing moments",
"onboardingFocusBody": "Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.",
"onboardingSendTwonliesTitle": "Send twonlies",
"onboardingSendTwonliesBody": "Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!",
"onboardingNotProductTitle": "You are not the product!",
"onboardingNotProductBody": "twonly is financed by donations and an optional subscription. Your data will never be sold.",
"onboardingBuyOneGetTwoTitle": "Buy one get two",
"onboardingBuyOneGetTwoBody": "twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.",
"onboardingGetStartedTitle": "Let's go!",
"onboardingGetStartedBody": "You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.",
"onboardingTryForFree": "Try for free",
"registerUsernameSlogan": "Please select a username so others can find you!",
"registerUsernameDecoration": "Username",
"registerUsernameLimits": "Your username must be at least 3 characters long.",
"registerProofOfWorkFailed": "There was an issue with the captcha test. Please try again.",
"registerSubmitButton": "Register now!",
"registerTwonlyCodeText": "Have you received a twonly code? Then redeem it either directly here or later!",
"registerTwonlyCodeLabel": "twonly-Code",
"newMessageTitle": "New message",
"chatsTapToSend": "Click to send your first image",
"cameraPreviewSendTo": "Send to",
"shareImageTitle": "Share with",
"shareImageBestFriends": "Best friends",
"shareImagePinnedContacts": "Pinnded",
"shareImagedEditorSendImage": "Send",
"shareImagedEditorShareWith": "Share with",
"shareImagedEditorSaveImage": "Save",
"shareImagedEditorSavedImage": "Saved",
"shareImageSearchAllContacts": "Search all contacts",
"startNewChatSearchHint": "Name, username or groupname",
"shareImagedSelectAll": "Select all",
"startNewChatTitle": "Select Contact",
"startNewChatNewContact": "New Contact",
"startNewChatYourContacts": "Your Contacts",
"shareImageAllUsers": "All contacts",
"shareImageAllTwonlyWarning": "twonlies can only be send to verified contacts!",
"shareImageUserNotVerified": "User is not verified",
"shareImageUserNotVerifiedDesc": "twonlies can only be sent to verified users. To verify a user, go to their profile and to verify security number.",
"shareImageShowArchived": "Show archived users",
"searchUsernameInput": "Username",
"searchUsernameTitle": "Search username",
"searchUserNamePreview": "To protect you and other twonly users from spam and abuse, it is not possible to search for other people in preview mode. Other users can find you and their requests will be displayed here!",
"selectSubscription": "Select subscription",
"searchUserNamePending": "Pending",
"searchUserNameBlockUserTooltip": "Block the user without informing.",
"searchUserNameRejectUserTooltip": "Reject the request and let the requester know.",
"searchUserNameArchiveUserTooltip": "Archive the user. He will appear again as soon as he accepts your request.",
"searchUsernameNotFound": "Username not found",
"searchUsernameNotFoundBody": "There is no user with the username \"{username}\" registered",
"@searchUsernameNotFoundBody": {
"placeholders": {
"username": {}
}
},
"searchUsernameNewFollowerTitle": "Follow requests",
"searchUsernameQrCodeBtn": "Scan QR code",
"chatListViewSearchUserNameBtn": "Add your first twonly contact!",
"chatListViewSendFirstTwonly": "Send your first twonly!",
"chatListDetailInput": "Type a message",
"userDeletedAccount": "The user has deleted its account.",
"contextMenuUserProfile": "User profile",
"contextMenuVerifyUser": "Verify",
"contextMenuArchiveUser": "Archive",
"contextMenuUndoArchiveUser": "Undo archiving",
"contextMenuOpenChat": "Open chat",
"contextMenuPin": "Pin",
"contextMenuUnpin": "Unpin",
"mediaViewerAuthReason": "Please authenticate to see this twonly!",
"mediaViewerTwonlyTapToOpen": "Tap to open your twonly!",
"messageSendState_Received": "Received",
"messageSendState_Opened": "Opened",
"messageSendState_Send": "Sent",
"messageSendState_Sending": "Sending",
"messageSendState_TapToLoad": "Tap to load",
"messageSendState_Loading": "Downloading",
"messageStoredInGallery": "Stored in gallery",
"messageReopened": "Re-opened",
"imageEditorDrawOk": "Take drawing",
"settingsTitle": "Settings",
"settingsChats": "Chats",
"settingsPreSelectedReactions": "Preselected reaction emojis",
"settingsPreSelectedReactionsError": "A maximum of 12 reactions can be selected.",
"settingsProfile": "Profile",
"settingsStorageData": "Data and storage",
"settingsStorageDataStoreInGTitle": "Store in Gallery",
"settingsStorageDataStoreInGSubtitle": "Store saved images additional in the systems gallery.",
"settingsStorageDataMediaAutoDownload": "Media auto-download",
"settingsStorageDataAutoDownMobile": "When using mobile data",
"settingsStorageDataAutoDownWifi": "When using WI-FI",
"settingsProfileCustomizeAvatar": "Customize your avatar",
"settingsProfileEditDisplayName": "Displayname",
"settingsProfileEditDisplayNameNew": "New Displayname",
"settingsAccount": "Konto",
"settingsSubscription": "Subscription",
"settingsAppearance": "Appearance",
"settingsPrivacy": "Privacy",
"settingsPrivacyBlockUsers": "Block users",
"settingsPrivacyBlockUsersDesc": "Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.",
"settingsPrivacyBlockUsersCount": "{len} contact(s)",
"@settingsPrivacyBlockUsersCount": {
"placeholders": {
"len": {}
}
},
"settingsNotification": "Notification",
"settingsNotifyTroubleshooting": "Troubleshooting",
"settingsNotifyTroubleshootingDesc": "Click here if you have problems receiving push notifications.",
"settingsNotifyTroubleshootingNoProblem": "No problem detected",
"settingsNotifyTroubleshootingNoProblemDesc": "Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.",
"settingsHelp": "Help",
"settingsHelpDiagnostics": "Diagnostic protocol",
"settingsHelpFAQ": "FAQ",
"feedbackTooltip": "Give Feedback to improve twonly.",
"settingsHelpContactUs": "Contact us",
"settingsHelpVersion": "Version",
"settingsHelpLicenses": "Licenses (Source-Code)",
"settingsHelpCredits": "Licenses (Images)",
"settingsHelpImprint": "Imprint & Privacy Policy",
"contactUsFaq": "Have you read our FAQ yet?",
"contactUsEmojis": "How do you feel? (optional)",
"contactUsSelectOption": "Please select an option",
"contactUsReason": "Tell us why you're reaching out",
"contactUsMessage": "If you want to receive an answer, please add your e-mail address so we can contact you.",
"contactUsYourMessage": "Your message",
"contactUsMessageTitle": "Tell us what's going on",
"contactUsReasonNotWorking": "Something's not working",
"contactUsReasonFeatureRequest": "Feature request",
"contactUsReasonQuestion": "Question",
"contactUsReasonFeedback": "Feedback",
"contactUsReasonOther": "Other",
"contactUsIncludeLog": "Include debug log",
"contactUsWhatsThat": "What's that?",
"contactUsLastWarning": "This are the information's which will be send to us. Please verify them and then press submit.",
"contactUsSuccess": "Feedback submitted successfully!",
"contactUsShortcut": "Hide Feedback Icon",
"settingsHelpTerms": "Terms of Service",
"settingsAppearanceTheme": "Theme",
"settingsAccountDeleteAccount": "Delete account",
"settingsAccountDeleteAccountWithBallance": "In the next step, you can select what you want to to with the remaining credit ({credit}).",
"settingsAccountDeleteAccountNoBallance": "Once you delete your account, there is no going back.",
"settingsAccountDeleteAccountNoInternet": "An Internet connection is required to delete your account.",
"settingsAccountDeleteModalTitle": "Are you sure?",
"settingsAccountDeleteModalBody": "Your account will be deleted. There is no change to restore it.",
"contactVerifyNumberTitle": "Verify safety number",
"contactVerifyNumberTapToScan": "Tap to scan",
"contactVerifyNumberMarkAsVerified": "Mark as verified",
"contactVerifyNumberClearVerification": "Clear verification",
"contactVerifyNumberLongDesc": "To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.",
"@contactVerifyNumberLongDesc": {
"placeholders": {
"username": {}
}
},
"contactNickname": "Nickname",
"contactNicknameNew": "New nickname",
"deleteAllContactMessages": "Delete all text-messages",
"deleteAllContactMessagesBody": "This will remove all messages, except stored media files, in your chat with {username}. This will NOT delete the messages stored at {username}s device!",
"@deleteAllContactMessagesBody": {
"placeholders": {
"username": {}
}
},
"contactBlock": "Block",
"contactBlockTitle": "Block {username}",
"@contactBlockTitle": {
"placeholders": {
"username": {}
}
},
"contactBlockBody": "A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.",
"contactRemove": "Remove user",
"contactRemoveTitle": "Remove {username}",
"contactRemoveBody": "Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT'S PHONE.",
"undo": "Undo",
"redo": "Redo",
"next": "Next",
"submit": "Submit",
"close": "Close",
"disable": "Disable",
"enable": "Enable",
"cancel": "Cancel",
"now": "Now",
"you": "You",
"minutesShort": "min.",
"image": "Image",
"video": "Video",
"react": "React",
"reply": "Reply",
"copy": "Copy",
"edit": "Edit",
"delete": "Delete",
"info": "Info",
"ok": "Ok",
"switchFrontAndBackCamera": "Switch between front and back camera.",
"addTextItem": "Text",
"protectAsARealTwonly": "Send as real twonly!",
"addDrawing": "Drawing",
"addEmoji": "Emoji",
"toggleFlashLight": "Toggle the flash light",
"toggleHighQuality": "Toggle better resolution",
"userFound": "{username} found",
"userFoundBody": "Do you want to create a follow request?",
"searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.",
"@searchUsernameNotFoundLong": {
"placeholders": {
"username": {}
}
},
"errorUnknown": "An unexpected error has occurred. Please try again later.",
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",
"errorTooManyRequests": "You have made too many requests in a short period. Please wait a moment before trying again.",
"errorInternalError": "The server is currently not available. Please try again later.",
"errorInvalidInvitationCode": "The invitation code you provided is invalid. Please check the code and try again.",
"errorUsernameAlreadyTaken": "The username is already taken.",
"errorSignatureNotValid": "The provided signature is not valid. Please check your credentials and try again.",
"errorUsernameNotFound": "The username you entered does not exist. Please check the spelling or create a new account.",
"errorUsernameNotValid": "The username you provided does not meet the required criteria. Please choose a valid username.",
"errorInvalidPublicKey": "The public key you provided is invalid. Please check the key and try again.",
"errorSessionAlreadyAuthenticated": "You are already logged in. Please log out if you want to log in with a different account.",
"errorSessionNotAuthenticated": "Your session is not authenticated. Please log in to continue.",
"errorOnlyOneSessionAllowed": "Only one active session is allowed per user. Please log out from other devices to continue.",
"errorNotEnoughCredit": "You do not have enough twonly-credit.",
"errorVoucherInvalid": "The voucher code you entered is not valid.",
"errorPlanLimitReached": "You have reached your plans limit. Please upgrade your plan.",
"errorPlanNotAllowed": "This feature is not available in your current plan.",
"errorPlanUpgradeNotYearly": "The plan upgrade must be paid for annually, as the current plan is also billed annually.",
"upgradeToPaidPlan": "Upgrade to a paid plan.",
"upgradeToPaidPlanButton": "Upgrade to {planId}{sufix}",
"partOfPaidPlanOf": "You are part of the paid plan of {username}!",
"year": "year",
"yearly": "Yearly",
"month": "month",
"monthly": "Monthly",
"proFeature1": "✓ Unlimited media file uploads",
"proFeature2": "✓ 1 additional Plus user",
"proFeature3": "✓ Restore flames",
"proFeature4": "✓ Support twonly",
"familyFeature1": "✓ Unlimited media file uploads",
"familyFeature2": "✓ 4 additional Plus user",
"familyFeature3": "✓ Restore flames",
"familyFeature4": "✓ Support twonly",
"redeemUserInviteCode": "Or redeem a twonly-Code.",
"redeemUserInviteCodeTitle": "Redeem twonly-Code",
"redeemUserInviteCodeSuccess": "Your plan has been successfully adjusted.",
"freeFeature1": "✓ 10 Media file uploads per day",
"plusFeature1": "✓ Unlimited media file uploads",
"plusFeature2": "✓ Additional features (coming-soon)",
"transactionHistory": "Your transaction history",
"manageSubscription": "Manage your subscription",
"nextPayment": "Next payment",
"currentBalance": "Current balance",
"manageAdditionalUsers": "Manage additional users",
"open": "Open",
"createOrRedeemVoucher": "Buy or redeem voucher",
"createVoucher": "Buy voucher",
"createVoucherDesc": "Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.",
"redeemVoucher": "Redeem voucher",
"openVouchers": "Open vouchers",
"voucherCreated": "Voucher created",
"voucherRedeemed": "Voucher redeemed",
"enterVoucherCode": "Enter Voucher Code",
"requestedVouchers": "Requested vouchers",
"redeemedVouchers": "Redeemed vouchers",
"buy": "Buy",
"subscriptionRefund": "When you upgrade, you will receive a refund of {refund} for your current subscription.",
"transactionCash": "Cash transaction",
"transactionPlanUpgrade": "Plan upgrade",
"transactionRefund": "Refund transaction",
"transactionThanksForTesting": "Thank you for testing",
"transactionUnknown": "Unknown transaction",
"transactionVoucherCreated": "Voucher created",
"transactionVoucherRedeemed": "Voucher redeemed",
"transactionAutoRenewal": "Automatic renewal",
"checkoutOptions": "Options",
"refund": "Refund",
"checkoutPayYearly": "Pay yearly",
"checkoutTotal": "Total",
"selectPaymentMethod": "Select Payment Method",
"twonlyCredit": "twonly-Credit",
"notEnoughCredit": "You do not have enough credit!",
"chargeCredit": "Charge credit",
"autoRenewal": "Auto renewal",
"autoRenewalDesc": "You can change this at any time.",
"autoRenewalLongDesc": "When your subscription expires, you will automatically be downgraded to the Preview plan. If you activate the automatic renewal, please make sure that you have enough credit for the automatic renewal. We will notify you in good time before the automatic renewal.",
"planSuccessUpgraded": "Successfully upgraded your plan.",
"checkoutSubmit": "Order with a fee.",
"additionalUsersList": "Your additional users",
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\" user",
"additionalUsersFreeTokens": "twonly-Codes für \"Free\" user",
"planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.",
"planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file.",
"galleryDelete": "Delete file",
"galleryDetails": "Show details",
"galleryExport": "Export to gallery",
"galleryExportSuccess": "Successfully saved in the Gallery.",
"settingsResetTutorials": "Show tutorials again",
"settingsResetTutorialsDesc": "Click here to show already displayed tutorials again.",
"settingsResetTutorialsSuccess": "Tutorials will be displayed again.",
"tutorialChatListSearchUsersTitle": "Find Friends and Manage Friend Requests",
"tutorialChatListSearchUsersDesc": "If you know your friends' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.",
"tutorialChatListContextMenuTitle": "Long press on the contact to open the context menu.",
"tutorialChatListContextMenuDesc": "With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.",
"tutorialChatMessagesVerifyShieldTitle": "Verify your contacts!",
"tutorialChatMessagesVerifyShieldDesc": "twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.",
"tutorialChatMessagesReopenMessageTitle": "Reopen Images and Videos",
"tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.",
"memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.",
"deleteTitle": "Are you sure?",
"deleteOkBtnForAll": "Delete for all",
"deleteOkBtnForMe": "Delete for me",
"deleteImageTitle": "Are you sure?",
"deleteImageBody": "The image will be irrevocably deleted.",
"settingsBackup": "Backup",
"backupNoticeTitle": "No backup configured",
"backupNoticeDesc": "If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.",
"backupNoticeLater": "Remind later",
"backupNoticeOpenBackup": "Create backup",
"backupPending": "Pending",
"backupFailed": "Failed",
"backupSuccess": "Success",
"backupTwonlySafeDesc": "Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.",
"backupNoPasswordRecovery": "Due to twonly's security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.",
"backupServer": "Server",
"backupMaxBackupSize": "max. backup size",
"backupStorageRetention": "Storage retention",
"backupLastBackupDate": "Last backup",
"backupLastBackupSize": "Backup size",
"backupLastBackupResult": "Result",
"deleteBackupTitle": "Are you sure?",
"deleteBackupBody": "Without an backup, you can not restore your user account.",
"backupData": "Data-Backup",
"backupDataDesc": "This backup contains besides of your twonly-Identity also all of your media files. This backup will is also encrypted but stored locally. You then have to ensure to manually copy it onto your laptop or device of your choice.",
"backupInsecurePassword": "Insecure password",
"backupInsecurePasswordDesc": "The chosen password is very insecure and can therefore easily be guessed by attackers. Please choose a secure password.",
"backupInsecurePasswordOk": "Continue anyway",
"backupInsecurePasswordCancel": "Try again",
"backupTwonlySafeLongDesc": "twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.",
"backupSelectStrongPassword": "Choose a secure password. This is required if you want to restore your twonly Backup.",
"password": "Password",
"passwordRepeated": "Repeat password",
"passwordRepeatedNotEqual": "Passwords do not match.",
"backupPasswordRequirement": "Password must be at least 8 characters long.",
"backupExpertSettings": "Expert settings",
"backupEnableBackup": "Activate automatic backup",
"backupOwnServerDesc": "Save your twonly Backup at twonly or on any server of your choice.",
"backupUseOwnServer": "Use server",
"backupResetServer": "Use standard server",
"backupTwonlySaveNow": "Save now",
"backupChangePassword": "Change password",
"twonlySafeRecoverTitle": "Recovery",
"twonlySafeRecoverDesc": "If you have created a backup with twonly Backup, you can restore it here.",
"twonlySafeRecoverBtn": "Restore backup",
"inviteFriends": "Invite your friends",
"inviteFriendsShareBtn": "Share",
"inviteFriendsShareText": "Let's switch to twonly: {url}",
"appOutdated": "Your version of twonly is out of date.",
"appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again",
"uploadLimitReached": "The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.",
"retransmissionRequested": "Retransmission requested",
"testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!",
"openChangeLog": "Open changelog automatically",
"reportUserTitle": "Report {username}",
"reportUserReason": "Reporting reason",
"reportUser": "Report user",
"newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.",
"tabToRemoveEmoji": "Tab to remove",
"quotedMessageWasDeleted": "The quoted message has been deleted.",
"messageWasDeleted": "Message has been deleted.",
"messageWasDeletedShort": "Deleted",
"sent": "Delivered",
"sentTo": "Delivered to",
"received": "Received",
"opened": "Opened",
"waitingForInternet": "Waiting for internet",
"editHistory": "Edit history",
"archivedChats": "Archived chats",
"durationShortSecond": "Sec.",
"durationShortMinute": "Min.",
"durationShortHour": "Hrs.",
"durationShortDays": "{count, plural, =1{1 Day} other{{count} Days}}",
"contacts": "Contacts",
"groups": "Groups",
"newGroup": "New group",
"selectMembers": "Select members",
"selectGroupName": "Select group name",
"groupNameInput": "Group name",
"groupMembers": "Members",
"addMember": "Add member",
"createGroup": "Create group",
"leaveGroup": "Leave group",
"createContactRequest": "Create contact request",
"contactRequestSend": "Contact request send",
"makeAdmin": "Make admin",
"removeAdmin": "Remove as admin",
"removeFromGroup": "Remove from group",
"admin": "Admin",
"revokeAdminRightsTitle": "Revoke {username}'s admin rights?",
"revokeAdminRightsOkBtn": "Remove as admin",
"makeAdminRightsTitle": "Make {username} an admin?",
"makeAdminRightsBody": "{username} will be able to edit this group and its members.",
"makeAdminRightsOkBtn": "Make admin",
"updateGroup": "Update group",
"alreadyInGroup": "Already in Group",
"removeContactFromGroupTitle": "Remove {username} from this group?",
"youChangedGroupName": "You have changed the group name to \"{newGroupName}\".",
"makerChangedGroupName": "{maker} has changed the group name to \"{newGroupName}\".",
"youCreatedGroup": "You have created the group.",
"makerCreatedGroup": "{maker} has created the group.",
"youRemovedMember": "You have removed {affected} from the group.",
"makerRemovedMember": "{maker} has removed {affected} from the group.",
"youAddedMember": "You have added {affected} to the group.",
"makerAddedMember": "{maker} has added {affected} to the group.",
"youMadeAdmin": "You made {affected} an admin.",
"makerMadeAdmin": "{maker} made {affected} an admin.",
"youRevokedAdminRights": "You revoked {affectedR} admin rights.",
"makerRevokedAdminRights": "{maker} revoked {affectedR} admin rights.",
"youLeftGroup": "You have left the group.",
"makerLeftGroup": "{maker} has left the group.",
"groupActionYou": "you",
"groupActionYour": "your",
"notificationFillerIn": "in",
"notificationText": "sent a message{inGroup}.",
"notificationTwonly": "sent a twonly{inGroup}.",
"notificationVideo": "sent a video{inGroup}.",
"notificationImage": "sent an image{inGroup}.",
"notificationAudio": "sent a voice message{inGroup}.",
"notificationAddedToGroup": "has added you to \"{groupname}\"",
"notificationContactRequest": "wants to connect with you.",
"notificationAcceptRequest": "is now connected with you.",
"notificationStoredMediaFile": "has stored your image.",
"notificationReaction": "has reacted to your image.",
"notificationReopenedMedia": "has reopened your image.",
"notificationReactionToVideo": "has reacted with {reaction} to your video.",
"notificationReactionToText": "has reacted with {reaction} to your message.",
"notificationReactionToImage": "has reacted with {reaction} to your image.",
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
"notificationResponse": "has responded{inGroup}.",
"notificationTitleUnknownUser": "[Unknown]",
"notificationCategoryMessageTitle": "Messages",
"notificationCategoryMessageDesc": "Messages from other users.",
"groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.",
"groupYouAreNowLongerAMember": "You are no longer part of this group.",
"groupNetworkIssue": "Network issue. Try again later.",
"leaveGroupSelectOtherAdminTitle": "Select another admin",
"leaveGroupSelectOtherAdminBody": "To leave the group, you must first select a new administrator.",
"leaveGroupSureTitle": "Leave group",
"leaveGroupSureBody": "Do you really want to leave the group?",
"leaveGroupSureOkBtn": "Leave group",
"changeDisplayMaxTime": "Chats will now be deleted after {time} ({username}).",
"youChangedDisplayMaxTime": "Chats will now be deleted after {time}.",
"userGotReported": "User has been reported.",
"deleteChatAfter": "Delete chat after...",
"deleteChatAfterAnHour": "one hour.",
"deleteChatAfterADay": "one day.",
"deleteChatAfterAWeek": "one week.",
"deleteChatAfterAMonth": "one month.",
"deleteChatAfterAYear": "one year.",
"yourTwonlyScore": "Your twonly-Score",
"registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.",
"dialogAskDeleteMediaFilePopTitle": "Are you sure you want to delete your masterpiece?",
"dialogAskDeleteMediaFilePopDelete": "Delete",
"allowErrorTracking": "Share errors and crashes with us",
"allowErrorTrackingSubtitle": "If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.",
"avatarSaveChanges": "Would you like to save the changes?",
"avatarSaveChangesStore": "Save",
"avatarSaveChangesDiscard": "Discard",
"inProcess": "In process",
"draftMessage": "Draft",
"exportMemories": "Export memories (Beta)",
"importMemories": "Import memories (Beta)",
"voiceMessageSlideToCancel": "Slide to cancel",
"voiceMessageCancel": "Cancel",
"shareYourProfile": "Share your profile",
"scanOtherProfile": "Scan other profile",
"skipForNow": "Skip for now",
"linkFromUsername": "Is the link from {username}?",
"linkFromUsernameLong": "If you received the link from your friend, you can mark the user as verified, as the public key in the link matches the public key already stored for that user?",
"gotLinkFromFriend": "Yes, I got the link from my friend!",
"couldNotVerifyUsername": "Could not verify {username}",
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!",
"startWithCameraOpen": "Start with camera open",
"showImagePreviewWhenSending": "Display image preview when selecting recipients",
"verifiedPublicKey": "The public key of {username} has been verified and is valid.",
"memoriesAYearAgo": "One year ago",
"memoriesXYearsAgo": "{years} years ago",
"migrationOfMemories": "Migration of media files: {open} still to be processed.",
"autoStoreAllSendUnlimitedMediaFiles": "Save all sent media",
"autoStoreAllSendUnlimitedMediaFilesSubtitle": "If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode."
}

View file

@ -7,6 +7,7 @@ import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_sv.dart';
// ignore_for_file: type=lint
@ -95,7 +96,8 @@ abstract class AppLocalizations {
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en')
Locale('en'),
Locale('sv')
];
/// No description provided for @registerTitle.
@ -1400,24 +1402,6 @@ abstract class AppLocalizations {
/// **'✓ Support twonly'**
String get familyFeature4;
/// No description provided for @redeemUserInviteCode.
///
/// In en, this message translates to:
/// **'Or redeem a twonly-Code.'**
String get redeemUserInviteCode;
/// No description provided for @redeemUserInviteCodeTitle.
///
/// In en, this message translates to:
/// **'Redeem twonly-Code'**
String get redeemUserInviteCodeTitle;
/// No description provided for @redeemUserInviteCodeSuccess.
///
/// In en, this message translates to:
/// **'Your plan has been successfully adjusted.'**
String get redeemUserInviteCodeSuccess;
/// No description provided for @freeFeature1.
///
/// In en, this message translates to:
@ -1445,7 +1429,7 @@ abstract class AppLocalizations {
/// No description provided for @manageSubscription.
///
/// In en, this message translates to:
/// **'Manage your subscription'**
/// **'Manage subscription'**
String get manageSubscription;
/// No description provided for @nextPayment.
@ -2887,6 +2871,54 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode.'**
String get autoStoreAllSendUnlimitedMediaFilesSubtitle;
/// No description provided for @termsOfService.
///
/// In en, this message translates to:
/// **'Terms of service'**
String get termsOfService;
/// No description provided for @privacyPolicy.
///
/// In en, this message translates to:
/// **'Privacy policy'**
String get privacyPolicy;
/// No description provided for @additionalUserAddError.
///
/// In en, this message translates to:
/// **'Could not add additional user. Try again later.'**
String get additionalUserAddError;
/// No description provided for @additionalUserAddButton.
///
/// In en, this message translates to:
/// **'Add additional user ({used}/{limit})'**
String additionalUserAddButton(Object limit, Object used);
/// No description provided for @additionalUserRemoveTitle.
///
/// In en, this message translates to:
/// **'Remove this additional user'**
String get additionalUserRemoveTitle;
/// No description provided for @additionalUserRemoveDesc.
///
/// In en, this message translates to:
/// **'After removal, the additional user will automatically be downgraded to the free plan, and you can add another person.'**
String get additionalUserRemoveDesc;
/// No description provided for @additionalUserSelectTitle.
///
/// In en, this message translates to:
/// **'Select additional users'**
String get additionalUserSelectTitle;
/// No description provided for @additionalUserSelectButton.
///
/// In en, this message translates to:
/// **'Select users ({used}/{limit})'**
String additionalUserSelectButton(Object limit, Object used);
}
class _AppLocalizationsDelegate
@ -2900,7 +2932,7 @@ class _AppLocalizationsDelegate
@override
bool isSupported(Locale locale) =>
<String>['de', 'en'].contains(locale.languageCode);
<String>['de', 'en', 'sv'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
@ -2913,6 +2945,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'sv':
return AppLocalizationsSv();
}
throw FlutterError(

View file

@ -734,16 +734,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get familyFeature4 => '✓ twonly unterstützen';
@override
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
@override
String get redeemUserInviteCodeTitle => 'twonly-Code einlösen';
@override
String get redeemUserInviteCodeSuccess =>
'Dein Plan wurde erfolgreich angepasst.';
@override
String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag';
@ -875,7 +865,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get checkoutSubmit => 'Kostenpflichtig bestellen';
@override
String get additionalUsersList => 'Ihre zusätzlichen Benutzer';
String get additionalUsersList => 'Deine zusätzlichen Benutzer';
@override
String get additionalUsersPlusTokens => 'twonly-Codes für \"Plus\"-Benutzer';
@ -1603,4 +1593,35 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get autoStoreAllSendUnlimitedMediaFilesSubtitle =>
'Wenn du diese Option aktivierst, werden alle Bilder, die du sendest, gespeichert, sofern sie mit einem unendlichen Countdown und nicht im twonly-Modus gesendet wurden.';
@override
String get termsOfService => 'Allgemeine Geschäftsbedingungen';
@override
String get privacyPolicy => 'Datenschutzerklärung';
@override
String get additionalUserAddError =>
'Es konnte kein zusätzlicher Nutzer hinzugefügt werden. Versuche es später noch einmal.';
@override
String additionalUserAddButton(Object limit, Object used) {
return 'Zusätzlichen Benutzer hinzufügen ($used/$limit)';
}
@override
String get additionalUserRemoveTitle =>
'Diesen zusätzlichen Benutzer entfernen';
@override
String get additionalUserRemoveDesc =>
'Der zusätzliche Nutzer wird nach der Entfernung automatisch auf den kostenlosen Tarif zurückgestuft und du kannst eine andere Person hinzufügen.';
@override
String get additionalUserSelectTitle => 'Zusätzliche Benutzer auswählen';
@override
String additionalUserSelectButton(Object limit, Object used) {
return 'Benutzer auswählen ($used/$limit)';
}
}

View file

@ -728,16 +728,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get familyFeature4 => '✓ Support twonly';
@override
String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
@override
String get redeemUserInviteCodeTitle => 'Redeem twonly-Code';
@override
String get redeemUserInviteCodeSuccess =>
'Your plan has been successfully adjusted.';
@override
String get freeFeature1 => '✓ 10 Media file uploads per day';
@ -751,7 +741,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get transactionHistory => 'Your transaction history';
@override
String get manageSubscription => 'Manage your subscription';
String get manageSubscription => 'Manage subscription';
@override
String get nextPayment => 'Next payment';
@ -1592,4 +1582,34 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get autoStoreAllSendUnlimitedMediaFilesSubtitle =>
'If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode.';
@override
String get termsOfService => 'Terms of service';
@override
String get privacyPolicy => 'Privacy policy';
@override
String get additionalUserAddError =>
'Could not add additional user. Try again later.';
@override
String additionalUserAddButton(Object limit, Object used) {
return 'Add additional user ($used/$limit)';
}
@override
String get additionalUserRemoveTitle => 'Remove this additional user';
@override
String get additionalUserRemoveDesc =>
'After removal, the additional user will automatically be downgraded to the free plan, and you can add another person.';
@override
String get additionalUserSelectTitle => 'Select additional users';
@override
String additionalUserSelectButton(Object limit, Object used) {
return 'Select users ($used/$limit)';
}
}

File diff suppressed because it is too large Load diff

@ -0,0 +1 @@
Subproject commit 2c12b3fe70dd47a4f4006205480e94172792beed

View file

@ -2260,6 +2260,69 @@ class ApplicationData_DeleteAccount extends $pb.GeneratedMessage {
static ApplicationData_DeleteAccount? _defaultInstance;
}
class ApplicationData_AddAdditionalUser extends $pb.GeneratedMessage {
factory ApplicationData_AddAdditionalUser({
$fixnum.Int64? userId,
}) {
final result = create();
if (userId != null) result.userId = userId;
return result;
}
ApplicationData_AddAdditionalUser._();
factory ApplicationData_AddAdditionalUser.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_AddAdditionalUser.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.AddAdditionalUser',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'userId')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_AddAdditionalUser clone() =>
ApplicationData_AddAdditionalUser()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_AddAdditionalUser copyWith(
void Function(ApplicationData_AddAdditionalUser) updates) =>
super.copyWith((message) =>
updates(message as ApplicationData_AddAdditionalUser))
as ApplicationData_AddAdditionalUser;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_AddAdditionalUser create() =>
ApplicationData_AddAdditionalUser._();
@$core.override
ApplicationData_AddAdditionalUser createEmptyInstance() => create();
static $pb.PbList<ApplicationData_AddAdditionalUser> createRepeated() =>
$pb.PbList<ApplicationData_AddAdditionalUser>();
@$core.pragma('dart2js:noInline')
static ApplicationData_AddAdditionalUser getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ApplicationData_AddAdditionalUser>(
create);
static ApplicationData_AddAdditionalUser? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get userId => $_getI64(0);
@$pb.TagNumber(1)
set userId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasUserId() => $_has(0);
@$pb.TagNumber(1)
void clearUserId() => $_clearField(1);
}
enum ApplicationData_ApplicationData {
textMessage,
getUserByUsername,
@ -2285,6 +2348,7 @@ enum ApplicationData_ApplicationData {
changeUsername,
ipaPurchase,
ipaForceCheck,
addAdditionalUser,
notSet
}
@ -2314,6 +2378,7 @@ class ApplicationData extends $pb.GeneratedMessage {
ApplicationData_ChangeUsername? changeUsername,
ApplicationData_IPAPurchase? ipaPurchase,
ApplicationData_IPAForceCheck? ipaForceCheck,
ApplicationData_AddAdditionalUser? addAdditionalUser,
}) {
final result = create();
if (textMessage != null) result.textMessage = textMessage;
@ -2348,6 +2413,7 @@ class ApplicationData extends $pb.GeneratedMessage {
if (changeUsername != null) result.changeUsername = changeUsername;
if (ipaPurchase != null) result.ipaPurchase = ipaPurchase;
if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck;
if (addAdditionalUser != null) result.addAdditionalUser = addAdditionalUser;
return result;
}
@ -2386,6 +2452,7 @@ class ApplicationData extends $pb.GeneratedMessage {
26: ApplicationData_ApplicationData.changeUsername,
27: ApplicationData_ApplicationData.ipaPurchase,
28: ApplicationData_ApplicationData.ipaForceCheck,
29: ApplicationData_ApplicationData.addAdditionalUser,
0: ApplicationData_ApplicationData.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -2417,7 +2484,8 @@ class ApplicationData extends $pb.GeneratedMessage {
25,
26,
27,
28
28,
29
])
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
protoName: 'textMessage',
@ -2508,6 +2576,10 @@ class ApplicationData extends $pb.GeneratedMessage {
28, _omitFieldNames ? '' : 'ipaForceCheck',
protoName: 'ipaForceCheck',
subBuilder: ApplicationData_IPAForceCheck.create)
..aOM<ApplicationData_AddAdditionalUser>(
29, _omitFieldNames ? '' : 'addAdditionalUser',
protoName: 'addAdditionalUser',
subBuilder: ApplicationData_AddAdditionalUser.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2822,6 +2894,18 @@ class ApplicationData extends $pb.GeneratedMessage {
void clearIpaForceCheck() => $_clearField(28);
@$pb.TagNumber(28)
ApplicationData_IPAForceCheck ensureIpaForceCheck() => $_ensure(23);
@$pb.TagNumber(29)
ApplicationData_AddAdditionalUser get addAdditionalUser => $_getN(24);
@$pb.TagNumber(29)
set addAdditionalUser(ApplicationData_AddAdditionalUser value) =>
$_setField(29, value);
@$pb.TagNumber(29)
$core.bool hasAddAdditionalUser() => $_has(24);
@$pb.TagNumber(29)
void clearAddAdditionalUser() => $_clearField(29);
@$pb.TagNumber(29)
ApplicationData_AddAdditionalUser ensureAddAdditionalUser() => $_ensure(24);
}
class Response_PreKey extends $pb.GeneratedMessage {

View file

@ -389,15 +389,6 @@ const ApplicationData$json = {
'9': 0,
'10': 'redeemAdditionalCode'
},
{
'1': 'removeAdditionalUser',
'3': 18,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.RemoveAdditionalUser',
'9': 0,
'10': 'removeAdditionalUser'
},
{
'1': 'updatePlanOptions',
'3': 19,
@ -479,6 +470,24 @@ const ApplicationData$json = {
'9': 0,
'10': 'ipaForceCheck'
},
{
'1': 'removeAdditionalUser',
'3': 18,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.RemoveAdditionalUser',
'9': 0,
'10': 'removeAdditionalUser'
},
{
'1': 'addAdditionalUser',
'3': 29,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.AddAdditionalUser',
'9': 0,
'10': 'addAdditionalUser'
},
],
'3': [
ApplicationData_TextMessage$json,
@ -504,7 +513,8 @@ const ApplicationData$json = {
ApplicationData_ReportUser$json,
ApplicationData_IPAPurchase$json,
ApplicationData_IPAForceCheck$json,
ApplicationData_DeleteAccount$json
ApplicationData_DeleteAccount$json,
ApplicationData_AddAdditionalUser$json
],
'8': [
{'1': 'ApplicationData'},
@ -714,6 +724,14 @@ const ApplicationData_DeleteAccount$json = {
'1': 'DeleteAccount',
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_AddAdditionalUser$json = {
'1': 'AddAdditionalUser',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
],
};
/// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
@ -740,48 +758,51 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'VudHNJbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0'
'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldEFkZGFjY291bnRzSW52aXRlcxJsChRyZWRlZW1BZG'
'RpdGlvbmFsQ29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl'
'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmwKFHJlbW92ZUFkZG'
'l0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVt'
'b3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoRdXBkYXRlUGxhbk'
'9wdGlvbnMYEyABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5VcGRhdGVQ'
'bGFuT3B0aW9uc0gAUhF1cGRhdGVQbGFuT3B0aW9ucxJUCgxkb3dubG9hZERvbmUYFCABKAsyLi'
'5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5Eb3dubG9hZERvbmVIAFIMZG93bmxv'
'YWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGBYgASgLMjkuY2xpZW50X3RvX3Nlcn'
'Zlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5QnlVc2VySWRIAFIXZ2V0U2lnbmVk'
'UHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2V5GBcgASgLMjQuY2xpZW50X3RvX3'
'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVkUHJlS2V5SABSEnVwZGF0ZVNpZ25l'
'ZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2xpZW50X3RvX3NlcnZlci5BcHBsaW'
'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJlcG9ydFVzZXIY'
'GSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcnRVc2VySABSCn'
'JlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFw'
'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRJRCgtpcGFQdX'
'JjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLklQQVB1cmNo'
'YXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2sYHCABKAsyLy5jbGllbnRfdG9fc2'
'VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNrSABSDWlwYUZvcmNlQ2hlY2saagoL'
'VGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZH'
'kSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0'
'VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZX'
'JuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tl'
'bhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl'
'9pZBgBIAEoA1IGdXNlcklkGikKDVJlZGVlbVZvdWNoZXISGAoHdm91Y2hlchgBIAEoCVIHdm91'
'Y2hlchpwChFTd2l0Y2hUb1BheWVkUGxhbhIXCgdwbGFuX2lkGAEgASgJUgZwbGFuSWQSHwoLcG'
'F5X21vbnRobHkYAiABKAhSCnBheU1vbnRobHkSIQoMYXV0b19yZW5ld2FsGAMgASgIUgthdXRv'
'UmVuZXdhbBo2ChFVcGRhdGVQbGFuT3B0aW9ucxIhCgxhdXRvX3JlbmV3YWwYASABKAhSC2F1dG'
'9SZW5ld2FsGjAKDUNyZWF0ZVZvdWNoZXISHwoLdmFsdWVfY2VudHMYASABKA1SCnZhbHVlQ2Vu'
'dHMaDQoLR2V0TG9jYXRpb24aDQoLR2V0Vm91Y2hlcnMaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFw'
'oVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUKE0dldEN1cnJlbnRQbGFuSW5mb3MaNwoUUmVkZWVt'
'QWRkaXRpb25hbENvZGUSHwoLaW52aXRlX2NvZGUYAiABKAlSCmludml0ZUNvZGUaLwoUUmVtb3'
'ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNC'
'eVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2'
'VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQ'
'c2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleR'
'gCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVz'
'aWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGA'
'EgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgB'
'IAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2'
'hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291'
'cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQU'
'ZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudEIRCg9BcHBsaWNhdGlvbkRhdGE=');
'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmMKEXVwZGF0ZVBsYW'
'5PcHRpb25zGBMgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRl'
'UGxhbk9wdGlvbnNIAFIRdXBkYXRlUGxhbk9wdGlvbnMSVAoMZG93bmxvYWREb25lGBQgASgLMi'
'4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRG93bmxvYWREb25lSABSDGRvd25s'
'b2FkRG9uZRJ1ChdnZXRTaWduZWRQcmVrZXlCeVVzZXJpZBgWIAEoCzI5LmNsaWVudF90b19zZX'
'J2ZXIuQXBwbGljYXRpb25EYXRhLkdldFNpZ25lZFByZUtleUJ5VXNlcklkSABSF2dldFNpZ25l'
'ZFByZWtleUJ5VXNlcmlkEmYKEnVwZGF0ZVNpZ25lZFByZWtleRgXIAEoCzI0LmNsaWVudF90b1'
'9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwZGF0ZVNpZ25lZFByZUtleUgAUhJ1cGRhdGVTaWdu'
'ZWRQcmVrZXkSVwoNZGVsZXRlQWNjb3VudBgYIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG'
'ljYXRpb25EYXRhLkRlbGV0ZUFjY291bnRIAFINZGVsZXRlQWNjb3VudBJOCgpyZXBvcnRVc2Vy'
'GBkgASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVwb3J0VXNlckgAUg'
'pyZXBvcnRVc2VyEloKDmNoYW5nZVVzZXJuYW1lGBogASgLMjAuY2xpZW50X3RvX3NlcnZlci5B'
'cHBsaWNhdGlvbkRhdGEuQ2hhbmdlVXNlcm5hbWVIAFIOY2hhbmdlVXNlcm5hbWUSUQoLaXBhUH'
'VyY2hhc2UYGyABKAsyLS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFQdXJj'
'aGFzZUgAUgtpcGFQdXJjaGFzZRJXCg1pcGFGb3JjZUNoZWNrGBwgASgLMi8uY2xpZW50X3RvX3'
'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuSVBBRm9yY2VDaGVja0gAUg1pcGFGb3JjZUNoZWNrEmwK'
'FHJlbW92ZUFkZGl0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG'
'lvbkRhdGEuUmVtb3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoR'
'YWRkQWRkaXRpb25hbFVzZXIYHSABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG'
'F0YS5BZGRBZGRpdGlvbmFsVXNlckgAUhFhZGRBZGRpdGlvbmFsVXNlchpqCgtUZXh0TWVzc2Fn'
'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2'
'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy'
'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX'
'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds'
'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg'
'Z1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGnAKEVN3'
'aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9udGhseR'
'gCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2FsGjYK'
'EVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3YWwaMA'
'oNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCgtHZXRM'
'b2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRBZGRBY2'
'NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdGlvbmFs'
'Q29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRpdGlvbm'
'FsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcK'
'B3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2'
'VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJl'
'a2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaW'
'duZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtl'
'eVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd2'
'5sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg5yZXBv'
'cnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJUEFQdXJjaGFzZRIdCgpwcm'
'9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgASgJUgZzb3VyY2USKwoRdmVy'
'aWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEaDwoNSVBBRm9yY2VDaGVjax'
'oPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEhcKB3VzZXJfaWQYASABKANS'
'BnVzZXJJZEIRCg9BcHBsaWNhdGlvbkRhdGE=');
@$core.Deprecated('Use responseDescriptor instead')
const Response$json = {

View file

@ -89,6 +89,8 @@ class ErrorCode extends $pb.ProtobufEnum {
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
static const ErrorCode IPAPaymentExpired =
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
static const ErrorCode UserIsNotInFreePlan =
ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan');
static const $core.List<ErrorCode> values = <ErrorCode>[
Unknown,
@ -128,6 +130,7 @@ class ErrorCode extends $pb.ProtobufEnum {
InvalidProofOfWork,
RegistrationDisabled,
IPAPaymentExpired,
UserIsNotInFreePlan,
];
static final $core.Map<$core.int, ErrorCode> _byValue =

View file

@ -55,6 +55,7 @@ const ErrorCode$json = {
{'1': 'InvalidProofOfWork', '2': 1032},
{'1': 'RegistrationDisabled', '2': 1033},
{'1': 'IPAPaymentExpired', '2': 1034},
{'1': 'UserIsNotInFreePlan', '2': 1035},
],
};
@ -76,4 +77,4 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
'UEFQYXltZW50RXhwaXJlZBCKCA==');
'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsI');

View file

@ -428,7 +428,6 @@ class Response_Plan extends $pb.GeneratedMessage {
$fixnum.Int64? dailyMediaUploadLimit,
$fixnum.Int64? maximalUploadSizeOfSingleMediaSize,
$fixnum.Int64? additionalPlusAccounts,
$fixnum.Int64? additionalFreeAccounts,
$fixnum.Int64? monthlyCostsCent,
$fixnum.Int64? yearlyCostsCent,
$core.bool? allowedToSendTextMessages,
@ -444,8 +443,6 @@ class Response_Plan extends $pb.GeneratedMessage {
maximalUploadSizeOfSingleMediaSize;
if (additionalPlusAccounts != null)
result.additionalPlusAccounts = additionalPlusAccounts;
if (additionalFreeAccounts != null)
result.additionalFreeAccounts = additionalFreeAccounts;
if (monthlyCostsCent != null) result.monthlyCostsCent = monthlyCostsCent;
if (yearlyCostsCent != null) result.yearlyCostsCent = yearlyCostsCent;
if (allowedToSendTextMessages != null)
@ -474,7 +471,6 @@ class Response_Plan extends $pb.GeneratedMessage {
..aInt64(3, _omitFieldNames ? '' : 'dailyMediaUploadLimit')
..aInt64(4, _omitFieldNames ? '' : 'maximalUploadSizeOfSingleMediaSize')
..aInt64(5, _omitFieldNames ? '' : 'additionalPlusAccounts')
..aInt64(6, _omitFieldNames ? '' : 'additionalFreeAccounts')
..aInt64(7, _omitFieldNames ? '' : 'monthlyCostsCent')
..aInt64(8, _omitFieldNames ? '' : 'yearlyCostsCent')
..aOB(9, _omitFieldNames ? '' : 'allowedToSendTextMessages')
@ -548,48 +544,39 @@ class Response_Plan extends $pb.GeneratedMessage {
@$pb.TagNumber(5)
void clearAdditionalPlusAccounts() => $_clearField(5);
@$pb.TagNumber(6)
$fixnum.Int64 get additionalFreeAccounts => $_getI64(5);
@$pb.TagNumber(6)
set additionalFreeAccounts($fixnum.Int64 value) => $_setInt64(5, value);
@$pb.TagNumber(6)
$core.bool hasAdditionalFreeAccounts() => $_has(5);
@$pb.TagNumber(6)
void clearAdditionalFreeAccounts() => $_clearField(6);
@$pb.TagNumber(7)
$fixnum.Int64 get monthlyCostsCent => $_getI64(6);
$fixnum.Int64 get monthlyCostsCent => $_getI64(5);
@$pb.TagNumber(7)
set monthlyCostsCent($fixnum.Int64 value) => $_setInt64(6, value);
set monthlyCostsCent($fixnum.Int64 value) => $_setInt64(5, value);
@$pb.TagNumber(7)
$core.bool hasMonthlyCostsCent() => $_has(6);
$core.bool hasMonthlyCostsCent() => $_has(5);
@$pb.TagNumber(7)
void clearMonthlyCostsCent() => $_clearField(7);
@$pb.TagNumber(8)
$fixnum.Int64 get yearlyCostsCent => $_getI64(7);
$fixnum.Int64 get yearlyCostsCent => $_getI64(6);
@$pb.TagNumber(8)
set yearlyCostsCent($fixnum.Int64 value) => $_setInt64(7, value);
set yearlyCostsCent($fixnum.Int64 value) => $_setInt64(6, value);
@$pb.TagNumber(8)
$core.bool hasYearlyCostsCent() => $_has(7);
$core.bool hasYearlyCostsCent() => $_has(6);
@$pb.TagNumber(8)
void clearYearlyCostsCent() => $_clearField(8);
@$pb.TagNumber(9)
$core.bool get allowedToSendTextMessages => $_getBF(8);
$core.bool get allowedToSendTextMessages => $_getBF(7);
@$pb.TagNumber(9)
set allowedToSendTextMessages($core.bool value) => $_setBool(8, value);
set allowedToSendTextMessages($core.bool value) => $_setBool(7, value);
@$pb.TagNumber(9)
$core.bool hasAllowedToSendTextMessages() => $_has(8);
$core.bool hasAllowedToSendTextMessages() => $_has(7);
@$pb.TagNumber(9)
void clearAllowedToSendTextMessages() => $_clearField(9);
@$pb.TagNumber(10)
$core.bool get isAdditionalAccount => $_getBF(9);
$core.bool get isAdditionalAccount => $_getBF(8);
@$pb.TagNumber(10)
set isAdditionalAccount($core.bool value) => $_setBool(9, value);
set isAdditionalAccount($core.bool value) => $_setBool(8, value);
@$pb.TagNumber(10)
$core.bool hasIsAdditionalAccount() => $_has(9);
$core.bool hasIsAdditionalAccount() => $_has(8);
@$pb.TagNumber(10)
void clearIsAdditionalAccount() => $_clearField(10);
}

View file

@ -220,13 +220,6 @@ const Response_Plan$json = {
'5': 3,
'10': 'additionalPlusAccounts'
},
{
'1': 'additional_free_accounts',
'3': 6,
'4': 1,
'5': 3,
'10': 'additionalFreeAccounts'
},
{
'1': 'monthly_costs_cent',
'3': 7,
@ -713,78 +706,77 @@ const Response_TransactionTypes$json = {
final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF'
'ICb2sSKAoFZXJyb3IYAiABKA4yEC5lcnJvci5FcnJvckNvZGVIAFIFZXJyb3IaIwoNQXV0aGVu'
'dGljYXRlZBISCgRwbGFuGAEgASgJUgRwbGFuGp4ECgRQbGFuEhcKB3BsYW5faWQYASABKAlSBn'
'dGljYXRlZBISCgRwbGFuGAEgASgJUgRwbGFuGuQDCgRQbGFuEhcKB3BsYW5faWQYASABKAlSBn'
'BsYW5JZBIqChF1cGxvYWRfc2l6ZV9saW1pdBgCIAEoA1IPdXBsb2FkU2l6ZUxpbWl0EjcKGGRh'
'aWx5X21lZGlhX3VwbG9hZF9saW1pdBgDIAEoA1IVZGFpbHlNZWRpYVVwbG9hZExpbWl0ElQKKG'
'1heGltYWxfdXBsb2FkX3NpemVfb2Zfc2luZ2xlX21lZGlhX3NpemUYBCABKANSIm1heGltYWxV'
'cGxvYWRTaXplT2ZTaW5nbGVNZWRpYVNpemUSOAoYYWRkaXRpb25hbF9wbHVzX2FjY291bnRzGA'
'UgASgDUhZhZGRpdGlvbmFsUGx1c0FjY291bnRzEjgKGGFkZGl0aW9uYWxfZnJlZV9hY2NvdW50'
'cxgGIAEoA1IWYWRkaXRpb25hbEZyZWVBY2NvdW50cxIsChJtb250aGx5X2Nvc3RzX2NlbnQYBy'
'ABKANSEG1vbnRobHlDb3N0c0NlbnQSKgoReWVhcmx5X2Nvc3RzX2NlbnQYCCABKANSD3llYXJs'
'eUNvc3RzQ2VudBJACh1hbGxvd2VkX3RvX3NlbmRfdGV4dF9tZXNzYWdlcxgJIAEoCFIZYWxsb3'
'dlZFRvU2VuZFRleHRNZXNzYWdlcxIyChVpc19hZGRpdGlvbmFsX2FjY291bnQYCiABKAhSE2lz'
'QWRkaXRpb25hbEFjY291bnQaPgoFUGxhbnMSNQoFcGxhbnMYASADKAsyHy5zZXJ2ZXJfdG9fY2'
'xpZW50LlJlc3BvbnNlLlBsYW5SBXBsYW5zGk0KEUFkZEFjY291bnRzSW52aXRlEhcKB3BsYW5f'
'aWQYASABKAlSBnBsYW5JZBIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRpcChJBZG'
'RBY2NvdW50c0ludml0ZXMSRgoHaW52aXRlcxgBIAMoCzIsLnNlcnZlcl90b19jbGllbnQuUmVz'
'cG9uc2UuQWRkQWNjb3VudHNJbnZpdGVSB2ludml0ZXMaxQEKC1RyYW5zYWN0aW9uEiMKDWRlcG'
'9zaXRfY2VudHMYASABKANSDGRlcG9zaXRDZW50cxJWChB0cmFuc2FjdGlvbl90eXBlGAIgASgO'
'Misuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5UcmFuc2FjdGlvblR5cGVzUg90cmFuc2FjdG'
'lvblR5cGUSOQoZY3JlYXRlZF9hdF91bml4X3RpbWVzdGFtcBgDIAEoA1IWY3JlYXRlZEF0VW5p'
'eFRpbWVzdGFtcBpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySW'
'QSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGr4BCgdWb3VjaGVyEh0KCnZvdWNoZXJfaWQYASAB'
'KAlSCXZvdWNoZXJJZBIfCgt2YWx1ZV9jZW50cxgCIAEoA1IKdmFsdWVDZW50cxIaCghyZWRlZW'
'1lZBgDIAEoCFIIcmVkZWVtZWQSHAoJcmVxdWVzdGVkGAQgASgIUglyZXF1ZXN0ZWQSOQoZY3Jl'
'YXRlZF9hdF91bml4X3RpbWVzdGFtcBgFIAEoA1IWY3JlYXRlZEF0VW5peFRpbWVzdGFtcBpKCg'
'hWb3VjaGVycxI+Cgh2b3VjaGVycxgBIAMoCzIiLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2Uu'
'Vm91Y2hlclIIdm91Y2hlcnMalwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlhX3'
'VwbG9hZF9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3Vw'
'bG9hZF9tZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQSMw'
'oTcGF5bWVudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCiBs'
'YXN0X3BheW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERvbm'
'VVbml4VGltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbGll'
'bnQuUmVzcG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2FjY2'
'91bnRzGAYgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3Vu'
'dFISYWRkaXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVuZX'
'dhbIgBARJCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb25h'
'bEFjY291bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3BheW'
'1lbnRfZG9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbmFs'
'X2FjY291bnRfb3duZXJfaWQaTgoITG9jYXRpb24SFgoGY291bnR5GAEgASgJUgZjb3VudHkSFg'
'oGcmVnaW9uGAIgASgJUgZyZWdpb24SEgoEY2l0eRgDIAEoCVIEY2l0eRowCgZQcmVLZXkSDgoC'
'aWQYASABKANSAmlkEhYKBnByZWtleRgCIAEoDFIGcHJla2V5GpUBCgxTaWduZWRQcmVLZXkSKA'
'oQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtl'
'eRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUh'
'VzaWduZWRQcmVrZXlTaWduYXR1cmUa9gMKCFVzZXJEYXRhEhcKB3VzZXJfaWQYASABKANSBnVz'
'ZXJJZBI7CgdwcmVrZXlzGAIgAygLMiEuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QcmVLZX'
'lSB3ByZWtleXMSHwoIdXNlcm5hbWUYByABKAxIAFIIdXNlcm5hbWWIAQESMwoTcHVibGljX2lk'
'ZW50aXR5X2tleRgDIAEoDEgBUhFwdWJsaWNJZGVudGl0eUtleYgBARIoCg1zaWduZWRfcHJla2'
'V5GAQgASgMSAJSDHNpZ25lZFByZWtleYgBARI7ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgF'
'IAEoDEgDUhVzaWduZWRQcmVrZXlTaWduYXR1cmWIAQESLQoQc2lnbmVkX3ByZWtleV9pZBgGIA'
'EoA0gEUg5zaWduZWRQcmVrZXlJZIgBARIsCg9yZWdpc3RyYXRpb25faWQYCCABKANIBVIOcmVn'
'aXN0cmF0aW9uSWSIAQFCCwoJX3VzZXJuYW1lQhYKFF9wdWJsaWNfaWRlbnRpdHlfa2V5QhAKDl'
'9zaWduZWRfcHJla2V5QhoKGF9zaWduZWRfcHJla2V5X3NpZ25hdHVyZUITChFfc2lnbmVkX3By'
'ZWtleV9pZEISChBfcmVnaXN0cmF0aW9uX2lkGlkKC1VwbG9hZFRva2VuEiEKDHVwbG9hZF90b2'
'tlbhgBIAEoDFILdXBsb2FkVG9rZW4SJwoPZG93bmxvYWRfdG9rZW5zGAIgAygMUg5kb3dubG9h'
'ZFRva2Vucxo5Cg5Eb3dubG9hZFRva2VucxInCg9kb3dubG9hZF90b2tlbnMYASADKAxSDmRvd2'
'5sb2FkVG9rZW5zGkUKC1Byb29mT2ZXb3JrEhYKBnByZWZpeBgBIAEoCVIGcHJlZml4Eh4KCmRp'
'ZmZpY3VsdHkYAiABKANSCmRpZmZpY3VsdHkawwcKAk9rEhQKBE5vbmUYASABKAhIAFIETm9uZR'
'IYCgZ1c2VyaWQYAiABKANIAFIGdXNlcmlkEiYKDWF1dGhjaGFsbGVuZ2UYAyABKAxIAFINYXV0'
'aGNoYWxsZW5nZRJKCgt1cGxvYWR0b2tlbhgEIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG'
'9uc2UuVXBsb2FkVG9rZW5IAFILdXBsb2FkdG9rZW4SQQoIdXNlcmRhdGEYBSABKAsyIy5zZXJ2'
'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVzZXJEYXRhSABSCHVzZXJkYXRhEh4KCWF1dGh0b2tlbh'
'gGIAEoDEgAUglhdXRodG9rZW4SQQoIbG9jYXRpb24YByABKAsyIy5zZXJ2ZXJfdG9fY2xpZW50'
'LlJlc3BvbnNlLkxvY2F0aW9uSABSCGxvY2F0aW9uElAKDWF1dGhlbnRpY2F0ZWQYCCABKAsyKC'
'5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkF1dGhlbnRpY2F0ZWRIAFINYXV0aGVudGljYXRl'
'ZBI4CgVwbGFucxgJIAEoCzIgLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbnNIAFIFcG'
'xhbnMSTQoMcGxhbmJhbGxhbmNlGAogASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Q'
'bGFuQmFsbGFuY2VIAFIMcGxhbmJhbGxhbmNlEkEKCHZvdWNoZXJzGAsgASgLMiMuc2VydmVyX3'
'RvX2NsaWVudC5SZXNwb25zZS5Wb3VjaGVyc0gAUgh2b3VjaGVycxJfChJhZGRhY2NvdW50c2lu'
'dml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aX'
'Rlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2'
'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk'
'0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVk'
'UHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b1'
'9jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2silgEKEFRy'
'YW5zYWN0aW9uVHlwZXMSCgoGUmVmdW5kEAASEwoPVm91Y2hlclJlZGVlbWVkEAESEgoOVm91Y2'
'hlckNyZWF0ZWQQAhIICgRDYXNoEAMSDwoLUGxhblVwZ3JhZGUQBBILCgdVbmtub3duEAUSFAoQ'
'VGhhbmtzRm9yVGVzdGluZxAGEg8KC0F1dG9SZW5ld2FsEAdCCgoIUmVzcG9uc2U=');
'UgASgDUhZhZGRpdGlvbmFsUGx1c0FjY291bnRzEiwKEm1vbnRobHlfY29zdHNfY2VudBgHIAEo'
'A1IQbW9udGhseUNvc3RzQ2VudBIqChF5ZWFybHlfY29zdHNfY2VudBgIIAEoA1IPeWVhcmx5Q2'
'9zdHNDZW50EkAKHWFsbG93ZWRfdG9fc2VuZF90ZXh0X21lc3NhZ2VzGAkgASgIUhlhbGxvd2Vk'
'VG9TZW5kVGV4dE1lc3NhZ2VzEjIKFWlzX2FkZGl0aW9uYWxfYWNjb3VudBgKIAEoCFITaXNBZG'
'RpdGlvbmFsQWNjb3VudBo+CgVQbGFucxI1CgVwbGFucxgBIAMoCzIfLnNlcnZlcl90b19jbGll'
'bnQuUmVzcG9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZB'
'gBIAEoCVIGcGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFj'
'Y291bnRzSW52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb2'
'5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxrFAQoLVHJhbnNhY3Rpb24SIwoNZGVwb3Np'
'dF9jZW50cxgBIAEoA1IMZGVwb3NpdENlbnRzElYKEHRyYW5zYWN0aW9uX3R5cGUYAiABKA4yKy'
'5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlRyYW5zYWN0aW9uVHlwZXNSD3RyYW5zYWN0aW9u'
'VHlwZRI5ChljcmVhdGVkX2F0X3VuaXhfdGltZXN0YW1wGAMgASgDUhZjcmVhdGVkQXRVbml4VG'
'ltZXN0YW1wGkUKEUFkZGl0aW9uYWxBY2NvdW50EhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIX'
'CgdwbGFuX2lkGAMgASgJUgZwbGFuSWQavgEKB1ZvdWNoZXISHQoKdm91Y2hlcl9pZBgBIAEoCV'
'IJdm91Y2hlcklkEh8KC3ZhbHVlX2NlbnRzGAIgASgDUgp2YWx1ZUNlbnRzEhoKCHJlZGVlbWVk'
'GAMgASgIUghyZWRlZW1lZBIcCglyZXF1ZXN0ZWQYBCABKAhSCXJlcXVlc3RlZBI5ChljcmVhdG'
'VkX2F0X3VuaXhfdGltZXN0YW1wGAUgASgDUhZjcmVhdGVkQXRVbml4VGltZXN0YW1wGkoKCFZv'
'dWNoZXJzEj4KCHZvdWNoZXJzGAEgAygLMiIuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Wb3'
'VjaGVyUgh2b3VjaGVycxqXBQoMUGxhbkJhbGxhbmNlEkAKHXVzZWRfZGFpbHlfbWVkaWFfdXBs'
'b2FkX2xpbWl0GAEgASgDUhl1c2VkRGFpbHlNZWRpYVVwbG9hZExpbWl0Ej4KHHVzZWRfdXBsb2'
'FkX21lZGlhX3NpemVfbGltaXQYAiABKANSGHVzZWRVcGxvYWRNZWRpYVNpemVMaW1pdBIzChNw'
'YXltZW50X3BlcmlvZF9kYXlzGAMgASgDSABSEXBheW1lbnRQZXJpb2REYXlziAEBEksKIGxhc3'
'RfcGF5bWVudF9kb25lX3VuaXhfdGltZXN0YW1wGAQgASgDSAFSHGxhc3RQYXltZW50RG9uZVVu'
'aXhUaW1lc3RhbXCIAQESSgoMdHJhbnNhY3Rpb25zGAUgAygLMiYuc2VydmVyX3RvX2NsaWVudC'
'5SZXNwb25zZS5UcmFuc2FjdGlvblIMdHJhbnNhY3Rpb25zEl0KE2FkZGl0aW9uYWxfYWNjb3Vu'
'dHMYBiADKAsyLC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZGl0aW9uYWxBY2NvdW50Uh'
'JhZGRpdGlvbmFsQWNjb3VudHMSJgoMYXV0b19yZW5ld2FsGAcgASgISAJSC2F1dG9SZW5ld2Fs'
'iAEBEkIKG2FkZGl0aW9uYWxfYWNjb3VudF9vd25lcl9pZBgIIAEoA0gDUhhhZGRpdGlvbmFsQW'
'Njb3VudE93bmVySWSIAQFCFgoUX3BheW1lbnRfcGVyaW9kX2RheXNCIwohX2xhc3RfcGF5bWVu'
'dF9kb25lX3VuaXhfdGltZXN0YW1wQg8KDV9hdXRvX3JlbmV3YWxCHgocX2FkZGl0aW9uYWxfYW'
'Njb3VudF9vd25lcl9pZBpOCghMb2NhdGlvbhIWCgZjb3VudHkYASABKAlSBmNvdW50eRIWCgZy'
'ZWdpb24YAiABKAlSBnJlZ2lvbhISCgRjaXR5GAMgASgJUgRjaXR5GjAKBlByZUtleRIOCgJpZB'
'gBIAEoA1ICaWQSFgoGcHJla2V5GAIgASgMUgZwcmVrZXkalQEKDFNpZ25lZFByZUtleRIoChBz'
'aWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GA'
'IgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNp'
'Z25lZFByZWtleVNpZ25hdHVyZRr2AwoIVXNlckRhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlck'
'lkEjsKB3ByZWtleXMYAiADKAsyIS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlByZUtleVIH'
'cHJla2V5cxIfCgh1c2VybmFtZRgHIAEoDEgAUgh1c2VybmFtZYgBARIzChNwdWJsaWNfaWRlbn'
'RpdHlfa2V5GAMgASgMSAFSEXB1YmxpY0lkZW50aXR5S2V5iAEBEigKDXNpZ25lZF9wcmVrZXkY'
'BCABKAxIAlIMc2lnbmVkUHJla2V5iAEBEjsKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgAS'
'gMSANSFXNpZ25lZFByZWtleVNpZ25hdHVyZYgBARItChBzaWduZWRfcHJla2V5X2lkGAYgASgD'
'SARSDnNpZ25lZFByZWtleUlkiAEBEiwKD3JlZ2lzdHJhdGlvbl9pZBgIIAEoA0gFUg5yZWdpc3'
'RyYXRpb25JZIgBAUILCglfdXNlcm5hbWVCFgoUX3B1YmxpY19pZGVudGl0eV9rZXlCEAoOX3Np'
'Z25lZF9wcmVrZXlCGgoYX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlQhMKEV9zaWduZWRfcHJla2'
'V5X2lkQhIKEF9yZWdpc3RyYXRpb25faWQaWQoLVXBsb2FkVG9rZW4SIQoMdXBsb2FkX3Rva2Vu'
'GAEgASgMUgt1cGxvYWRUb2tlbhInCg9kb3dubG9hZF90b2tlbnMYAiADKAxSDmRvd25sb2FkVG'
'9rZW5zGjkKDkRvd25sb2FkVG9rZW5zEicKD2Rvd25sb2FkX3Rva2VucxgBIAMoDFIOZG93bmxv'
'YWRUb2tlbnMaRQoLUHJvb2ZPZldvcmsSFgoGcHJlZml4GAEgASgJUgZwcmVmaXgSHgoKZGlmZm'
'ljdWx0eRgCIAEoA1IKZGlmZmljdWx0eRrDBwoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgK'
'BnVzZXJpZBgCIAEoA0gAUgZ1c2VyaWQSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2'
'hhbGxlbmdlEkoKC3VwbG9hZHRva2VuGAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z'
'ZS5VcGxvYWRUb2tlbkgAUgt1cGxvYWR0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuVXNlckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYg'
'ASgMSABSCWF1dGh0b2tlbhJBCghsb2NhdGlvbhgHIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUm'
'VzcG9uc2UuTG9jYXRpb25IAFIIbG9jYXRpb24SUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNl'
'cnZlcl90b19jbGllbnQuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEj'
'gKBXBsYW5zGAkgASgLMiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFu'
'cxJNCgxwbGFuYmFsbGFuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW'
'5CYWxsYW5jZUgAUgxwbGFuYmFsbGFuY2USQQoIdm91Y2hlcnMYCyABKAsyIy5zZXJ2ZXJfdG9f'
'Y2xpZW50LlJlc3BvbnNlLlZvdWNoZXJzSABSCHZvdWNoZXJzEl8KEmFkZGFjY291bnRzaW52aX'
'RlcxgMIAEoCzItLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuQWRkQWNjb3VudHNJbnZpdGVz'
'SABSEmFkZGFjY291bnRzaW52aXRlcxJTCg5kb3dubG9hZHRva2VucxgNIAEoCzIpLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuRG93bmxvYWRUb2tlbnNIAFIOZG93bmxvYWR0b2tlbnMSTQoM'
'c2lnbmVkcHJla2V5GA4gASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5TaWduZWRQcm'
'VLZXlIAFIMc2lnbmVkcHJla2V5EkoKC3Byb29mT2ZXb3JrGA8gASgLMiYuc2VydmVyX3RvX2Ns'
'aWVudC5SZXNwb25zZS5Qcm9vZk9mV29ya0gAUgtwcm9vZk9mV29ya0IECgJPayKWAQoQVHJhbn'
'NhY3Rpb25UeXBlcxIKCgZSZWZ1bmQQABITCg9Wb3VjaGVyUmVkZWVtZWQQARISCg5Wb3VjaGVy'
'Q3JlYXRlZBACEggKBENhc2gQAxIPCgtQbGFuVXBncmFkZRAEEgsKB1Vua25vd24QBRIUChBUaG'
'Fua3NGb3JUZXN0aW5nEAYSDwoLQXV0b1JlbmV3YWwQB0IKCghSZXNwb25zZQ==');

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
@ -120,15 +121,36 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
}
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
Log.info('verifying _verifyPurchase');
if (Platform.isIOS) {
try {
var b64Data = purchaseDetails.verificationData.serverVerificationData
.split('.')[1];
final paddingNeeded = (4 - (b64Data.length % 4)) % 4;
b64Data += '=' * paddingNeeded;
final jsonData = base64Decode(b64Data);
final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
final expiresDate = data['expiresDate'] as int;
final dt =
DateTime.fromMillisecondsSinceEpoch(expiresDate, isUtc: true);
if (dt.isBefore(DateTime.now())) {
Log.warn('ExpiresDate is in the past: $dt');
if (_userTriggeredBuyButton && Platform.isIOS) {
await launchUrl(
Uri.parse('https://apps.apple.com/account/subscriptions'),
mode: LaunchMode.externalApplication,
);
}
return false;
}
} catch (e) {
Log.error(e);
}
}
if (kDebugMode) {
Log.info(purchaseDetails.productID);
Log.info(purchaseDetails.verificationData.serverVerificationData);
// if (Platform.isIOS) {
// final data = purchaseDetails.verificationData.serverVerificationData;
// printWrapped(data);
// final datas = data.split('.')[1];
// printWrapped(datas);
// }
Log.info(purchaseDetails.verificationData.source);
}
final res = await apiService.ipaPurchase(
@ -163,7 +185,9 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
Log.info(
'_handlePurchase: ${purchaseDetails.productID}, ${purchaseDetails.status}',
);
if (purchaseDetails.status == PurchaseStatus.purchased) {
if (purchaseDetails.status == PurchaseStatus.purchased ||
(purchaseDetails.status == PurchaseStatus.restored &&
_userTriggeredBuyButton)) {
await _verifyPurchase(purchaseDetails);
}
if (purchaseDetails.status == PurchaseStatus.restored &&

View file

@ -595,20 +595,6 @@ class ApiService {
return null;
}
Future<List<Response_AddAccountsInvite>?> getAdditionalUserInvites() async {
final get = ApplicationData_GetAddAccountsInvites();
final appData = ApplicationData()..getAddaccountsInvites = get;
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasAddaccountsinvites()) {
return ok.addaccountsinvites.invites;
}
}
return null;
}
Future<Result> updatePlanOptions(bool autoRenewal) async {
final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
final appData = ApplicationData()..updatePlanOptions = get;
@ -623,6 +609,13 @@ class ApiService {
return sendRequestSync(req, contactId: userId.toInt());
}
Future<Result> addAdditionalUser(Int64 userId) async {
final get = ApplicationData_AddAdditionalUser()..userId = userId;
final appData = ApplicationData()..addAdditionalUser = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req, contactId: userId.toInt());
}
Future<Result> buyVoucher(int valueInCents) async {
final get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
final appData = ApplicationData()..createVoucher = get;

View file

@ -60,18 +60,18 @@ class _AccountViewState extends State<AccountView> {
),
body: ListView(
children: [
ListTile(
title: const Text('Transfer account'),
subtitle: const Text('Coming soon'),
onTap: () async {
await showAlertDialog(
context,
'Coming soon',
'This feature is not yet implemented!',
);
},
),
const Divider(),
// ListTile(
// title: const Text('Transfer account'),
// subtitle: const Text('Coming soon'),
// onTap: () async {
// await showAlertDialog(
// context,
// 'Coming soon',
// 'This feature is not yet implemented!',
// );
// },
// ),
// const Divider(),
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
@ -99,15 +99,6 @@ class _AccountViewState extends State<AccountView> {
),
)
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
onLongPress: !kReleaseMode
? () async {
await deleteLocalUserData();
await Restart.restartApp(
notificationTitle: 'Account successfully deleted',
notificationBody: 'Click here to open the app again',
);
}
: null,
onTap: (formattedBallance == null)
? null
: () async {

View file

@ -1,8 +1,10 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart';
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart';
@ -64,6 +66,23 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
);
},
),
ListTile(
title: const Text('Delete all (!) app data'),
onTap: () async {
final ok = await showAlertDialog(
context,
'Sure?',
'If you do not have a backup, you have to register with a new account.',
);
if (ok) {
await deleteLocalUserData();
await Restart.restartApp(
notificationTitle: 'Account successfully deleted',
notificationBody: 'Click here to open the app again',
);
}
},
),
ListTile(
title: const Text('Disable ffmpeg'),
subtitle: const Text(

View file

@ -1,39 +1,19 @@
import 'dart:async';
import 'dart:convert';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/subscription/select_additional_users.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async {
final ballance = await apiService.getAdditionalUserInvites();
if (ballance != null) {
await updateUserdata((u) {
u.additionalUserInvites =
jsonEncode(ballance.map((x) => x.writeToJson()).toList());
return u;
});
return ballance;
}
if (gUser.lastPlanBallance != null) {
try {
final decoded = jsonDecode(gUser.additionalUserInvites!) as List<String>;
return decoded.map(Response_AddAccountsInvite.fromJson).toList();
} catch (e) {
Log.error('could not parse additional user json: $e');
}
}
return null;
}
class AdditionalUsersView extends StatefulWidget {
const AdditionalUsersView({required this.ballance, super.key});
@ -44,42 +24,94 @@ class AdditionalUsersView extends StatefulWidget {
}
class _AdditionalUsersViewState extends State<AdditionalUsersView> {
List<Response_AddAccountsInvite>? additionalInvites;
Response_PlanBallance? ballance;
late int _unusedAdditionalAccounts;
int _planLimit = 0;
@override
void initState() {
super.initState();
ballance = widget.ballance;
final currentPlan = context.read<PurchasesProvider>().plan;
if (currentPlan == SubscriptionPlan.Pro ||
currentPlan == SubscriptionPlan.Family) {
_planLimit = 1;
} else if (currentPlan == SubscriptionPlan.Family) {
_planLimit = 4;
}
_unusedAdditionalAccounts =
_planLimit - (ballance?.additionalAccounts.length ?? _planLimit);
unawaited(initAsync(force: false));
}
Future<void> initAsync({required bool force}) async {
additionalInvites = await loadAdditionalUserInvites();
if (force) {
ballance = await loadPlanBalance();
_unusedAdditionalAccounts =
_planLimit - (ballance?.additionalAccounts.length ?? _planLimit);
}
setState(() {});
}
Future<void> addAdditionalUser() async {
final selectedUserIds = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SelectAdditionalUsers(
limit: _planLimit,
alreadySelected: ballance?.additionalAccounts
.map((e) => e.userId.toInt())
.toList() ??
[],
),
),
) as List<int>?;
if (selectedUserIds == null) return;
for (final selectedUserId in selectedUserIds) {
final res = await apiService.addAdditionalUser(Int64(selectedUserId));
if (res.isError && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.additionalUserAddError)),
);
}
}
await initAsync(force: true);
}
@override
Widget build(BuildContext context) {
var plusInvites = <Response_AddAccountsInvite>[];
if (additionalInvites != null) {
plusInvites =
additionalInvites!.where((x) => x.planId == 'Plus').toList();
}
return Scaffold(
appBar: AppBar(
title: Text(context.lang.manageAdditionalUsers),
),
body: ListView(
children: [
if (_unusedAdditionalAccounts > 0)
Center(
child: Padding(
padding: const EdgeInsets.all(12),
child: FilledButton(
onPressed: addAdditionalUser,
child: Text(
context.lang.additionalUserAddButton(
_planLimit,
ballance?.additionalAccounts.length ?? 0,
),
),
),
),
),
if (ballance != null && ballance!.additionalAccounts.isNotEmpty)
ListTile(
title: Text(
context.lang.additionalUsersList,
style: const TextStyle(fontSize: 13),
Padding(
padding: const EdgeInsets.only(top: 10),
child: ListTile(
title: Text(
context.lang.additionalUsersList,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
),
),
if (ballance != null)
@ -91,22 +123,6 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
},
),
),
if (plusInvites.isNotEmpty)
Text(
context.lang.additionalUsersPlusTokens,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
Padding(
padding: const EdgeInsets.all(16),
child: GridView.count(
crossAxisCount: 2,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 16 / 5,
shrinkWrap: true,
children: plusInvites.map(AdditionalUserInvite.new).toList(),
),
),
],
),
);
@ -178,8 +194,8 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
onPressed: () async {
final remove = await showAlertDialog(
context,
'Remove this additional user',
'The additional user will automatically be downgraded to the free plan after removal and you will receive a new invitation code to give to another person.',
context.lang.additionalUserRemoveTitle,
context.lang.additionalUserRemoveDesc,
);
if (remove) {
final res = await apiService
@ -208,40 +224,3 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
);
}
}
class AdditionalUserInvite extends StatefulWidget {
const AdditionalUserInvite(this.invite, {super.key});
final Response_AddAccountsInvite invite;
@override
State<AdditionalUserInvite> createState() => _AdditionalUserInviteState();
}
class _AdditionalUserInviteState extends State<AdditionalUserInvite> {
Future<void> _copyVoucherId() async {
await Clipboard.setData(ClipboardData(text: widget.invite.inviteCode));
await HapticFeedback.heavyImpact();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${widget.invite.inviteCode} copied.')),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _copyVoucherId,
child: Card(
elevation: 3,
child: Center(
child: Text(
widget.invite.inviteCode.toUpperCase(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}

View file

@ -0,0 +1,256 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart';
class SelectAdditionalUsers extends StatefulWidget {
const SelectAdditionalUsers({
required this.alreadySelected,
required this.limit,
super.key,
});
final List<int> alreadySelected;
final int limit;
@override
State<SelectAdditionalUsers> createState() => _SelectAdditionalUsers();
}
class _SelectAdditionalUsers extends State<SelectAdditionalUsers> {
List<Contact> contacts = [];
List<Contact> allContacts = [];
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Contact>> contactSub;
final HashSet<int> selectedUsers = HashSet();
late HashSet<int> _alreadySelected;
@override
void initState() {
super.initState();
_alreadySelected = HashSet.from(widget.alreadySelected);
final stream = twonlyDB.contactsDao.watchAllAcceptedContacts();
contactSub = stream.listen((update) async {
update.sort(
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
);
setState(() {
allContacts = update;
});
await filterUsers();
});
}
@override
void dispose() {
unawaited(contactSub.cancel());
super.dispose();
}
Future<void> filterUsers() async {
if (searchUserName.value.text.isEmpty) {
setState(() {
contacts = allContacts;
});
return;
}
final usersFiltered = allContacts
.where(
(user) => getContactDisplayName(user)
.toLowerCase()
.contains(searchUserName.value.text.toLowerCase()),
)
.toList();
setState(() {
contacts = usersFiltered;
});
}
void toggleSelectedUser(int userId) {
if (_alreadySelected.contains(userId)) return;
if (!selectedUsers.contains(userId)) {
selectedUsers.add(userId);
} else {
selectedUsers.remove(userId);
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: Text(context.lang.additionalUserSelectTitle),
),
floatingActionButton: FilledButton.icon(
onPressed: selectedUsers.isEmpty
? null
: () => Navigator.pop(context, selectedUsers.toList()),
label: Text(
context.lang.additionalUserSelectButton(
widget.limit,
selectedUsers.length + widget.alreadySelected.length,
),
),
icon: const FaIcon(FontAwesomeIcons.userPlus),
),
body: SafeArea(
child: Padding(
padding:
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(
onChanged: (_) async {
await filterUsers();
},
controller: searchUserName,
decoration: getInputDecoration(
context,
context.lang.shareImageSearchAllContacts,
),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
restorationId: 'new_message_users_list',
itemCount:
contacts.length + (selectedUsers.isEmpty ? 0 : 2),
itemBuilder: (BuildContext context, int i) {
if (selectedUsers.isNotEmpty) {
final selected = selectedUsers.toList();
if (i == 0) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 18),
constraints: const BoxConstraints(
maxHeight: 150,
),
child: SingleChildScrollView(
child: LayoutBuilder(
builder: (context, constraints) {
return Wrap(
spacing: 8,
children: selected.map((w) {
return _Chip(
contact: allContacts
.firstWhere((t) => t.userId == w),
onTap: toggleSelectedUser,
);
}).toList(),
);
},
),
),
);
}
if (i == 1) {
return const Divider();
}
i -= 2;
}
final user = contacts[i];
return UserContextMenu(
key: ValueKey(user.userId),
contact: user,
child: ListTile(
title: Row(
children: [
Text(getContactDisplayName(user)),
FlameCounterWidget(
contactId: user.userId,
prefix: true,
),
],
),
subtitle: (_alreadySelected.contains(user.userId))
? Text(context.lang.alreadyInGroup)
: null,
leading: AvatarIcon(
contactId: user.userId,
fontSize: 13,
),
trailing: Checkbox(
value: selectedUsers.contains(user.userId) |
_alreadySelected.contains(user.userId),
side: WidgetStateBorderSide.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return const BorderSide(width: 0);
}
return BorderSide(
color: Theme.of(context).colorScheme.outline,
);
},
),
onChanged: (bool? value) {
toggleSelectedUser(user.userId);
},
),
onTap: () {
toggleSelectedUser(user.userId);
},
),
);
},
),
),
],
),
),
),
),
);
}
}
class _Chip extends StatelessWidget {
const _Chip({
required this.contact,
required this.onTap,
});
final Contact contact;
final void Function(int) onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onTap(contact.userId),
child: Chip(
avatar: AvatarIcon(
contactId: contact.userId,
fontSize: 10,
),
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
getContactDisplayName(contact),
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 15),
const FaIcon(
FontAwesomeIcons.xmark,
color: Colors.grey,
size: 12,
),
],
),
),
);
}
}

View file

@ -7,7 +7,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/model/purchases/purchasable_product.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
@ -115,26 +114,33 @@ class _SubscriptionViewState extends State<SubscriptionView> {
onPurchase: initAsync,
),
],
if (currentPlan == SubscriptionPlan.Free) ...[
const SizedBox(height: 10),
Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: Text(
context.lang.redeemUserInviteCode,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
),
),
const SizedBox(height: 10),
PlanCard(
plan: SubscriptionPlan.Plus,
onPurchase: initAsync,
),
],
const SizedBox(height: 10),
if (currentPlan != SubscriptionPlan.Family) const Divider(),
BetterListTile(
icon: FontAwesomeIcons.fileContract,
text: context.lang.termsOfService,
trailing:
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
onTap: () async {
await launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html'));
},
),
BetterListTile(
leading: const FaIcon(
FontAwesomeIcons.gavel,
size: 15,
),
text: context.lang.privacyPolicy,
trailing:
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
onTap: () async {
await launchUrl(
Uri.parse('https://twonly.eu/de/legal/privacy.html'),
);
},
),
if (isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
const Divider(),
if (isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
BetterListTile(
@ -169,7 +175,7 @@ class PlanCard extends StatefulWidget {
this.paidMonthly,
});
final SubscriptionPlan plan;
final void Function()? onPurchase;
final Future<void> Function()? onPurchase;
final bool? paidMonthly;
@override
@ -177,24 +183,22 @@ class PlanCard extends StatefulWidget {
}
String getFormattedPrice(PurchasableProduct product) {
if (product.price.contains('')) {
return product.price.replaceAll(',00', '').replaceAll('.00', '');
}
return product.price;
}
class _PlanCardState extends State<PlanCard> {
PurchasableProduct? _isLoading;
Future<void> onButtonPressed(PurchasableProduct? product) async {
if (widget.onPurchase == null) return;
if (widget.plan == SubscriptionPlan.Free ||
widget.plan == SubscriptionPlan.Plus) {
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
widget.onPurchase!();
return;
}
if (widget.onPurchase == null || _isLoading != null) return;
if (product == null) return;
setState(() {
_isLoading = product;
});
await context.read<PurchasesProvider>().buy(product);
widget.onPurchase!();
await widget.onPurchase!();
setState(() {
_isLoading = null;
});
}
@override
@ -263,43 +267,6 @@ class _PlanCardState extends State<PlanCard> {
fontWeight: FontWeight.bold,
),
),
if (yearlyProduct != null && currentPlan != widget.plan)
const SizedBox(height: 10),
if (yearlyProduct != null &&
widget.paidMonthly == null &&
currentPlan != widget.plan)
Column(
children: [
Text(
'${getFormattedPrice(yearlyProduct)}/${context.lang.year}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
if (monthlyProduct != null)
Text(
'${getFormattedPrice(monthlyProduct)}/${context.lang.month}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
if (widget.paidMonthly != null)
Text(
(widget.paidMonthly!)
? '${getFormattedPrice(monthlyProduct!)}/${context.lang.month}'
: '${getFormattedPrice(yearlyProduct!)}/${context.lang.year}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 10),
@ -330,31 +297,43 @@ class _PlanCardState extends State<PlanCard> {
),
if (widget.onPurchase != null && monthlyProduct != null)
OutlinedButton.icon(
onPressed: () => onButtonPressed(monthlyProduct),
label: (widget.plan == SubscriptionPlan.Free ||
widget.plan == SubscriptionPlan.Plus)
? Text(context.lang.redeemUserInviteCodeTitle)
: Text(
context.lang.upgradeToPaidPlanButton(
widget.plan.name,
' (${context.lang.monthly})',
),
),
onPressed: _isLoading != null
? null
: () => onButtonPressed(monthlyProduct),
icon: _isLoading == monthlyProduct
? const SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1),
)
: null,
label: Text(
context.lang.upgradeToPaidPlanButton(
widget.plan.name,
' (${getFormattedPrice(monthlyProduct)}/${context.lang.month})',
),
),
),
if (widget.onPurchase != null &&
(yearlyProduct != null ||
currentPlan == SubscriptionPlan.Free))
FilledButton.icon(
onPressed: () => onButtonPressed(yearlyProduct),
label: (widget.plan == SubscriptionPlan.Free ||
widget.plan == SubscriptionPlan.Plus)
? Text(context.lang.redeemUserInviteCodeTitle)
: Text(
context.lang.upgradeToPaidPlanButton(
widget.plan.name,
' (${context.lang.yearly})',
),
),
onPressed: _isLoading != null
? null
: () => onButtonPressed(yearlyProduct),
icon: _isLoading == yearlyProduct
? const SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1),
)
: null,
label: Text(
context.lang.upgradeToPaidPlanButton(
widget.plan.name,
' (${getFormattedPrice(yearlyProduct!)}/${context.lang.year})',
),
),
),
],
),
@ -363,75 +342,3 @@ class _PlanCardState extends State<PlanCard> {
);
}
}
Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
var inviteCode = '';
// ignore: inference_failure_on_function_invocation
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(context.lang.redeemUserInviteCodeTitle),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: TextField(
onChanged: (value) => setState(() {
inviteCode = value.toUpperCase();
}),
decoration: InputDecoration(
labelText: context.lang.registerTwonlyCodeLabel,
border: const OutlineInputBorder(),
),
textCapitalization: TextCapitalization.characters,
),
),
],
),
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.lang.cancel),
),
TextButton(
onPressed: () async {
final res = await apiService.redeemUserInviteCode(inviteCode);
if (!context.mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.redeemUserInviteCodeSuccess),
),
);
// reconnect to load new plan.
await apiService.close(() {});
await apiService.connect();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorCodeToText(context, res.error as ErrorCode),
),
),
);
}
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: Text(context.lang.ok),
),
],
);
},
);
}

View file

@ -226,27 +226,6 @@ class _SubscriptionCustomViewState extends State<SubscriptionCustomView> {
await initAsync();
},
),
if (!isPayingUser(currentPlan)) ...[
const SizedBox(height: 10),
Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: Text(
context.lang.redeemUserInviteCode,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
),
),
const SizedBox(height: 10),
PlanCard(
plan: SubscriptionPlan.Plus,
onTap: () async {
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
await initAsync();
},
),
],
const SizedBox(height: 10),
if (currentPlan != SubscriptionPlan.Family) const Divider(),
BetterListTile(
@ -479,13 +458,9 @@ class PlanCard extends StatelessWidget {
padding: const EdgeInsets.only(top: 10),
child: FilledButton.icon(
onPressed: onTap,
label: (plan == SubscriptionPlan.Free ||
plan == SubscriptionPlan.Plus)
? Text(context.lang.redeemUserInviteCodeTitle)
: Text(
context.lang
.upgradeToPaidPlanButton(plan.name, ''),
),
label: Text(
context.lang.upgradeToPaidPlanButton(plan.name, ''),
),
),
),
],
@ -496,75 +471,3 @@ class PlanCard extends StatelessWidget {
);
}
}
Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
var inviteCode = '';
// ignore: inference_failure_on_function_invocation
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(context.lang.redeemUserInviteCodeTitle),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: TextField(
onChanged: (value) => setState(() {
inviteCode = value.toUpperCase();
}),
decoration: InputDecoration(
labelText: context.lang.registerTwonlyCodeLabel,
border: const OutlineInputBorder(),
),
textCapitalization: TextCapitalization.characters,
),
),
],
),
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.lang.cancel),
),
TextButton(
onPressed: () async {
final res = await apiService.redeemUserInviteCode(inviteCode);
if (!context.mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.redeemUserInviteCodeSuccess),
),
);
// reconnect to load new plan.
await apiService.close(() {});
await apiService.connect();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorCodeToText(context, res.error as ErrorCode),
),
),
);
}
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: Text(context.lang.ok),
),
],
);
},
);
}