mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +00:00
commit
39122476ce
68 changed files with 1825 additions and 821 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -56,3 +56,4 @@ devtools_options.yaml
|
|||
rust/target
|
||||
rust_dependencies/target
|
||||
fastlane/repo/status/running.json
|
||||
.cache/
|
||||
224
CHANGELOG.md
224
CHANGELOG.md
|
|
@ -1,5 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
## 0.5.1
|
||||
|
||||
- Improve: Show delivery and read receipt indicators for text messages
|
||||
- Improve: Backup screen clearer and easier to understand
|
||||
- Improve: Show username above messages in group chats
|
||||
- Fix: Background audio correctly pauses and resumes when viewing videos
|
||||
- Fix: Sometimes old messages were being received as duplicates
|
||||
- Fix: Multiple smaller bug fixes
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- New: Update to Signal's new PQC-ready key agreement PQXDH
|
||||
|
|
@ -49,8 +58,8 @@
|
|||
|
||||
## 0.3.0
|
||||
|
||||
- Improved: Design of some UI components
|
||||
- Improved: Memories viewer shows state for batch operations and has improved performance
|
||||
- Improve: Design of some UI components
|
||||
- Improve: Memories viewer shows state for batch operations and has improved performance
|
||||
- Fix: Issue with background notifications on Android
|
||||
- Fix: Changed minimum threshold for the user discovery to 3
|
||||
- Fix: Multiple UI issues
|
||||
|
|
@ -59,24 +68,24 @@
|
|||
## 0.2.26
|
||||
|
||||
- New: Import images from the gallery
|
||||
- Improved: Media files are now stored in the dedicated "twonly" album
|
||||
- Improved: UI components adapt to native styling (iOS/Android)
|
||||
- Improve: Media files are now stored in the dedicated "twonly" album
|
||||
- Improve: UI components adapt to native styling (iOS/Android)
|
||||
- Fix: Migration issue that resulted in a corrupted backup mechanism
|
||||
- Fix: Database issues causing messages to be lost or the database to be corrupted
|
||||
- Fix: Permission view did not disappear after they were granted
|
||||
|
||||
## 0.2.23
|
||||
|
||||
- Improved: Smaller UI changes
|
||||
- Improve: Smaller UI changes
|
||||
- Fix: Some messages were not marked as opened.
|
||||
|
||||
## 0.2.20
|
||||
|
||||
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
||||
- New: Adds security profiles.
|
||||
- Improved: Onboarding flow for new users.
|
||||
- Improved: Flame restore experience.
|
||||
- Improved: The blue verification checkmark now displays the total number of verifications.
|
||||
- Improve: Onboarding flow for new users.
|
||||
- Improve: Flame restore experience.
|
||||
- Improve: The blue verification checkmark now displays the total number of verifications.
|
||||
- Fix: Issue with receiving messages when user closed app while decrypting
|
||||
- Fix: Background message fetching reliability.
|
||||
- Fix: Issue with focus changing when taking a picture
|
||||
|
|
@ -95,22 +104,22 @@
|
|||
|
||||
- New: Tutorial on how to use zoom.
|
||||
- New: Manage storage view.
|
||||
- Improved: Media thumbnails for faster loading.
|
||||
- Improve: Media thumbnails for faster loading.
|
||||
- Fix: Some messages were not marked as opened.
|
||||
|
||||
## 0.2.12
|
||||
|
||||
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
||||
- Improved: Memories viewer redesigned with smoother animations and new quick-action controls.
|
||||
- Improve: Memories viewer redesigned with smoother animations and new quick-action controls.
|
||||
- Fix: Reliability of receiving media files.
|
||||
|
||||
## 0.2.11
|
||||
|
||||
- New: Create custom shortcuts to quickly share images with pre-selected groups
|
||||
- New: Seamless recovery for iOS reinstallations
|
||||
- Improved: Redesigned snackbar notifications
|
||||
- Improved: New backup mechanism to allow larger backup files
|
||||
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
|
||||
- Improve: Redesigned snackbar notifications
|
||||
- Improve: New backup mechanism to allow larger backup files
|
||||
- Improve: Move keys into a centralized Rust-owned structure stored in secure storage
|
||||
- Fix: Messages occasionally not received until app restart
|
||||
- Fix: Multiple smaller issues
|
||||
|
||||
|
|
@ -120,7 +129,7 @@
|
|||
|
||||
## 0.2.9
|
||||
|
||||
- Improved: Make contact avatars clickable
|
||||
- Improve: Make contact avatars clickable
|
||||
- Fix: Messages occasionally not received until app restart
|
||||
- Fix: Complete setup would sometimes get stuck
|
||||
|
||||
|
|
@ -133,21 +142,21 @@
|
|||
- New: Feature to find friends without a phone number
|
||||
- New: The verification state is now transferred to the scanned user
|
||||
- New: Registration setup to configure the most important configurations
|
||||
- Improved: Show ⌛ instead of the flame icon when it is about to expire
|
||||
- Improved: FAQ is now in the app rather than opening in the browser
|
||||
- Improved: Videos can now be paused
|
||||
- Improved: Lock to record hands-free
|
||||
- Improve: Show ⌛ instead of the flame icon when it is about to expire
|
||||
- Improve: FAQ is now in the app rather than opening in the browser
|
||||
- Improve: Videos can now be paused
|
||||
- Improve: Lock to record hands-free
|
||||
- Fix: Many smaller issues
|
||||
|
||||
## 0.1.8
|
||||
|
||||
- Improved: Typos and grammar issues thanks to @AlbertUnruh
|
||||
- Improve: Typos and grammar issues thanks to @AlbertUnruh
|
||||
- Fix: App becomes unresponsive when clicking notifications.
|
||||
|
||||
## 0.1.7
|
||||
|
||||
- Improved: Show input indicator in the chat overview as well
|
||||
- Improved: Username change error handling
|
||||
- Improve: Show input indicator in the chat overview as well
|
||||
- Improve: Username change error handling
|
||||
- Fix: Phantom push notification
|
||||
- Fix: Start in chat, if configured
|
||||
- Fix: Smaller UI fixes
|
||||
|
|
@ -199,7 +208,7 @@
|
|||
|
||||
## 0.0.96
|
||||
|
||||
- Feature: Show link in chat if the saved media file contains one
|
||||
- New: Show link in chat if the saved media file contains one
|
||||
- Improve: Verification badge for groups
|
||||
- Improve: Huge reduction in app size
|
||||
- Fix: Crash on older devices when compressing a video
|
||||
|
|
@ -207,7 +216,7 @@
|
|||
|
||||
## 0.0.93
|
||||
|
||||
- Feature: Verification checkmark for friends
|
||||
- New: Verification checkmark for friends
|
||||
- Fix: Added contacts in contact sharing that were not clickable.
|
||||
- Fix: Open chat after the image expires in case a draft message exists
|
||||
- Fix: Restore flames as a plus user
|
||||
|
|
@ -220,139 +229,138 @@
|
|||
|
||||
## 0.0.92
|
||||
|
||||
- Adds the option to share contacts
|
||||
- Adds option to zoom in received images / videos
|
||||
- Fixes issue with "reuploaded requested" not working
|
||||
- Fixes race condition while writing to the log file
|
||||
- New: The option to share contacts
|
||||
- New: Option to zoom in received images / videos
|
||||
- Fix: Issue with "reuploaded requested" not working
|
||||
- Fix: Race condition while writing to the log file
|
||||
|
||||
## 0.0.91
|
||||
|
||||
- Fixes link preview on iOS
|
||||
- Fixes sharing images from other apps on iOS
|
||||
- Fix: Link preview on iOS
|
||||
- Fix: Sharing images from other apps on iOS
|
||||
|
||||
## 0.0.90
|
||||
|
||||
- Fixes issue that media files where not reuploaded
|
||||
- Fixes iOS zooming issue when switching between .5 and x1
|
||||
- Fixes biometric auth bypass when opening a twonly/reopen send image
|
||||
- Fixes that media files could not be downloaded in case the contact deleted his account
|
||||
- Fixes database issue in case twonly is opened multiple times
|
||||
- Fixes typos in translation
|
||||
- Fix: Issue that media files were not reuploaded
|
||||
- Fix: iOS zooming issue when switching between .5 and x1
|
||||
- Fix: Biometric auth bypass when opening a twonly/reopen send image
|
||||
- Fix: That media files could not be downloaded in case the contact deleted his account
|
||||
- Fix: Database issue in case twonly is opened multiple times
|
||||
- Fix: Typos in translation
|
||||
|
||||
## 0.0.87
|
||||
|
||||
- Adds link preview to shared links
|
||||
- Adds option to manual focus in the camera
|
||||
- Adds support to switch between front and back cameras during video recording
|
||||
- Adds basic face filters
|
||||
- Improves image editor, like emojis or text under a drawing can be moved
|
||||
- Improves speed after taking a picture
|
||||
- Fixes issue with emojis disappearing in the image editor
|
||||
- New: Link preview to shared links
|
||||
- New: Option to manual focus in the camera
|
||||
- New: Support to switch between front and back cameras during video recording
|
||||
- New: Basic face filters
|
||||
- Improve: Image editor, like emojis or text under a drawing can be moved
|
||||
- Improve: Speed after taking a picture
|
||||
- Fix: Issue with emojis disappearing in the image editor
|
||||
|
||||
## 0.0.86
|
||||
|
||||
- Allows to reopen send images (if send without time limit or enabled auth)
|
||||
- Added support for front camera zoom
|
||||
- Several bug fixes
|
||||
- New: Allows to reopen send images (if send without time limit or enabled auth)
|
||||
- New: Support for front camera zoom
|
||||
- Fix: Several bug fixes
|
||||
|
||||
## 0.0.83
|
||||
|
||||
- Improved view of the diagnostic log
|
||||
- Several bug fixes
|
||||
- Improve: View of the diagnostic log
|
||||
- Fix: Several bug fixes
|
||||
|
||||
## 0.0.82
|
||||
|
||||
- Added an option in the settings to automatically save all sent images
|
||||
- Hides duplicate images in the memory
|
||||
- Fixes a bug where messages were not being received
|
||||
- Several other minor improvements
|
||||
- New: An option in the settings to automatically save all sent images
|
||||
- New: Hides duplicate images in the memory
|
||||
- Improve: Several other minor improvements
|
||||
- Fix: A bug where messages were not being received
|
||||
|
||||
## 0.0.81
|
||||
|
||||
- Fixes the issue where black/blank images were sometimes received
|
||||
- Fixes an issue in the image editor
|
||||
- Fix: The issue where black/blank images were sometimes received
|
||||
- Fix: An issue in the image editor
|
||||
|
||||
## 0.0.80
|
||||
|
||||
- Share images/videos directly from other applications
|
||||
- More customization options in the appearance settings
|
||||
- Improved UI for changing the display time of images
|
||||
- Several minor UI improvements
|
||||
- Several bug fixes
|
||||
- New: Share images/videos directly from other applications
|
||||
- New: More customization options in the appearance settings
|
||||
- Improve: UI for changing the display time of images
|
||||
- Improve: Several minor UI improvements
|
||||
- Fix: Several bug fixes
|
||||
|
||||
## 0.0.74
|
||||
|
||||
- Improving uploading speed
|
||||
- Fixing issue with ffmpeg for android
|
||||
- Improve: Uploading speed
|
||||
- Fix: Issue with ffmpeg for android
|
||||
|
||||
## 0.0.73
|
||||
|
||||
- Integrated QR code scanner in the main camera
|
||||
- New profile share page
|
||||
- New workflow for checking the security number
|
||||
- Improved user interface for creating voice messages
|
||||
- New: Integrated QR code scanner in the main camera
|
||||
- New: Profile share page
|
||||
- New: Workflow for checking the security number
|
||||
- Improve: User interface for creating voice messages
|
||||
|
||||
## 0.0.69
|
||||
|
||||
- Option to export and import memories
|
||||
- iOS support for ultra-wide-angle camera
|
||||
- Support Android Monochrome Icon
|
||||
- Multiple layout issues fixed
|
||||
- Multiple bug fixes
|
||||
- New: Option to export and import memories
|
||||
- New: iOS support for ultra-wide-angle camera
|
||||
- New: Support Android Monochrome Icon
|
||||
- Fix: Multiple layout issues fixed
|
||||
- Fix: Multiple bug fixes
|
||||
|
||||
## 0.0.67
|
||||
|
||||
- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
|
||||
- Fixes bug when saving images to the gallery
|
||||
- Multiple layout issues fixed
|
||||
- Multiple bug fixes
|
||||
- New: Crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
|
||||
- Fix: Bug when saving images to the gallery
|
||||
- Fix: Multiple layout issues fixed
|
||||
- Fix: Multiple bug fixes
|
||||
|
||||
## 0.0.62
|
||||
|
||||
- Support for groups with multiple administrators
|
||||
- Edit and delete messages
|
||||
- Create images using volume buttons
|
||||
- New and improved emoji picker
|
||||
- Removing audio after recording is possible
|
||||
- Edited image is now embedded into the video
|
||||
- Video max length increased to 60 seconds
|
||||
- Switched to FFmpeg for improved video compression
|
||||
- New context menu and other UI enhancements
|
||||
- Client-to-client protocol migrated to Protocol Buffers (Protobuf)
|
||||
- Database identifiers converted to UUIDs and the database schema completely redesigned
|
||||
- Improved reliability of client-to-client messaging
|
||||
- Multiple bug fixes
|
||||
- New: Support for groups with multiple administrators
|
||||
- New: Edit and delete messages
|
||||
- New: Create images using volume buttons
|
||||
- New: Removing audio after recording is possible
|
||||
- New: Edited image is now embedded into the video
|
||||
- New: Video max length increased to 60 seconds
|
||||
- New: Context menu and other UI enhancements
|
||||
- New: Client-to-client protocol migrated to Protocol Buffers (Protobuf)
|
||||
- New: Database identifiers converted to UUIDs and the database schema completely redesigned
|
||||
- Improve: Emoji picker
|
||||
- Improve: Switched to FFmpeg for improved video compression
|
||||
- Improve: Reliability of client-to-client messaging
|
||||
- Fix: Multiple bug fixes
|
||||
|
||||
## 0.0.61
|
||||
|
||||
- Improving image editor when changing colors
|
||||
- Fixing message decryption error
|
||||
- Fixing issue with user deletion
|
||||
- Fixing issue with flame counter sync
|
||||
- Dependency and Flutter upgrade
|
||||
- New: Image editor when changing colors
|
||||
- New: Dependency and Flutter upgrade
|
||||
- Fix: Message decryption error
|
||||
- Fix: Issue with user deletion
|
||||
- Fix: Issue with flame counter sync
|
||||
|
||||
## 0.0.60
|
||||
|
||||
- Improved logging to debug the 'Tap to load' issue.
|
||||
- Display your own avatar in the title bar of the chat list.
|
||||
- Created a default avatar image in case none was set.
|
||||
- Improved UI handling when requesting microphone access for the first time.
|
||||
- Flutter SDK and dependencies upgraded.
|
||||
- Multiple bug fixes.
|
||||
- New: Display your own avatar in the title bar of the chat list.
|
||||
- New: Created a default avatar image in case none was set.
|
||||
- New: Flutter SDK and dependencies upgraded.
|
||||
- Improve: Logging to debug the 'Tap to load' issue.
|
||||
- Improve: UI handling when requesting microphone access for the first time.
|
||||
- Fix: Multiple bug fixes.
|
||||
|
||||
## 0.0.59
|
||||
|
||||
- Fixing media download error
|
||||
- Fixing issue with video recording
|
||||
- Location Filter are now stored as WebP instead of PNG
|
||||
- New: Location Filter are now stored as WebP instead of PNG
|
||||
- Fix: Media download error
|
||||
- Fix: Issue with video recording
|
||||
|
||||
## 0.0.58
|
||||
|
||||
- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon)
|
||||
- iOS gestures to close images
|
||||
- Improved chat messages view, including better citation view and display times
|
||||
- Onboarding screens updated and registration view simplified
|
||||
- The sender is displayed in the top right corner when a media file is opened
|
||||
- Images are now stored as WebP to save storage
|
||||
- Button to report users
|
||||
- Multiple bug fixes
|
||||
- New: iOS gestures to close images
|
||||
- New: Onboarding screens updated and registration view simplified
|
||||
- New: The sender is displayed in the top right corner when a media file is opened
|
||||
- New: Images are now stored as WebP to save storage
|
||||
- New: Button to report users
|
||||
- Improve: Chat messages view, including better citation view and display times
|
||||
- Fix: Multiple bug fixes
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ analyzer:
|
|||
- "**.arb"
|
||||
- "test/drift/**"
|
||||
- "**.g.dart"
|
||||
- build/**
|
||||
- android/**
|
||||
- ios/**
|
||||
- web/**
|
||||
- windows/**
|
||||
- macos/**
|
||||
- linux/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ android {
|
|||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
manifestPlaceholders = [appName: "twonly"]
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
|
|
@ -53,6 +54,7 @@ android {
|
|||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".testing"
|
||||
manifestPlaceholders = [appName: "twonly [dev]"]
|
||||
}
|
||||
// profile {
|
||||
// applicationIdSuffix ".STOP"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<application
|
||||
android:label="twonly"
|
||||
android:label="${appName}"
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7
|
||||
Subproject commit b874b9cc309822d90b940fdfbc8fd1ad3afc2450
|
||||
204
dependencies.py
Normal file
204
dependencies.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import yaml
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
def print_blue(text):
|
||||
BLUE = '\x1b[34m'
|
||||
RESET = '\x1b[0m'
|
||||
print(f"{BLUE}{text}{RESET}")
|
||||
|
||||
def print_yellow(text):
|
||||
YELLOW = '\x1b[33m'
|
||||
RESET = '\x1b[0m'
|
||||
print(f"{YELLOW}{text}{RESET}")
|
||||
|
||||
def get_git_head(repo_path='.'):
|
||||
result = subprocess.run(
|
||||
['git', 'rev-parse', 'HEAD'],
|
||||
cwd=repo_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"git error: {result.stderr.strip()}")
|
||||
return result.stdout.strip()
|
||||
|
||||
def get_default_branch(repo_path='.'):
|
||||
result = subprocess.run(
|
||||
['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'],
|
||||
cwd=repo_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip().split('/')[-1]
|
||||
return 'main'
|
||||
|
||||
def integrate_package(folder_name, data, cache_dir, out_dir):
|
||||
repo_url = data['git']
|
||||
keep_list = ["lib", "LICENSE", "pubspec.yaml", "android", "ios", "darwin"]
|
||||
if "keep" in data:
|
||||
keep_list += [item.rstrip('/') for item in data['keep']]
|
||||
|
||||
print(f"Processing {folder_name}...")
|
||||
|
||||
cache_path = os.path.join(cache_dir, folder_name)
|
||||
if not os.path.exists(cache_path):
|
||||
subprocess.run(["git", "clone", repo_url, cache_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
result = subprocess.run(["git", "fetch", "--all"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
||||
if result.returncode != 0:
|
||||
print_yellow(f"Warning: Could not fetch updates for {folder_name}. You might be offline.")
|
||||
|
||||
if "commit" in data:
|
||||
commit_hash = data["commit"]
|
||||
subprocess.run(["git", "checkout", commit_hash], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
||||
elif "tag" in data:
|
||||
tag_name = data["tag"]
|
||||
subprocess.run(["git", "checkout", tag_name], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
||||
else:
|
||||
print_yellow(f"Warning: No commit or tag specified for {folder_name}. Using default branch.")
|
||||
default_branch = get_default_branch(cache_path)
|
||||
subprocess.run(["git", "checkout", default_branch], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
||||
subprocess.run(["git", "pull"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path)
|
||||
last_commit_hash = get_git_head(cache_path)
|
||||
data["commit"] = last_commit_hash
|
||||
print_blue(f"Recorded commit {last_commit_hash} for {folder_name}")
|
||||
|
||||
results = [] # List of (pkg_name, version)
|
||||
|
||||
if "subpackages" in data:
|
||||
packages_to_extract = data["subpackages"]
|
||||
else:
|
||||
packages_to_extract = [{"name": folder_name, "path": data.get("path", "")}]
|
||||
|
||||
for pkg in packages_to_extract:
|
||||
pkg_name = pkg["name"]
|
||||
subpath = pkg.get("path", "")
|
||||
|
||||
out_path = os.path.join(out_dir, pkg_name)
|
||||
if os.path.exists(out_path):
|
||||
shutil.rmtree(out_path)
|
||||
os.makedirs(out_path)
|
||||
|
||||
package_src_path = os.path.join(cache_path, subpath) if subpath else cache_path
|
||||
|
||||
for item in keep_list:
|
||||
src_item = os.path.join(package_src_path, item)
|
||||
dst_item = os.path.join(out_path, item)
|
||||
|
||||
if os.path.exists(src_item):
|
||||
os.makedirs(os.path.dirname(dst_item), exist_ok=True)
|
||||
if os.path.isdir(src_item):
|
||||
shutil.copytree(src_item, dst_item, dirs_exist_ok=True)
|
||||
else:
|
||||
shutil.copy2(src_item, dst_item)
|
||||
|
||||
version = "any"
|
||||
try:
|
||||
pubspec_path = os.path.join(package_src_path, "pubspec.yaml")
|
||||
if os.path.exists(pubspec_path):
|
||||
with open(pubspec_path, "r") as f:
|
||||
ps = yaml.safe_load(f)
|
||||
if ps and isinstance(ps, dict):
|
||||
version = ps.get("version", "any")
|
||||
except Exception as e:
|
||||
print_yellow(f"Warning: Could not read version from {pkg_name}/pubspec.yaml")
|
||||
|
||||
results.append((pkg_name, version))
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Update specific or all repositories.")
|
||||
parser.add_argument('repo_name', nargs='?', default=None, help="Name of the repository to update (optional)")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open("dependencies.yaml", "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
cache_dir = config.get('cache', './.cache')
|
||||
out_dir = config.get('outdir', './dependencies')
|
||||
deps = config.get('dependencies', {})
|
||||
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
if not os.path.exists(out_dir):
|
||||
os.makedirs(out_dir)
|
||||
|
||||
repos_to_update = [args.repo_name] if args.repo_name else list(deps.keys())
|
||||
|
||||
pubspec_overrides = []
|
||||
pubspec_deps = []
|
||||
|
||||
def process_deps_recursive(deps_dict, to_update=None):
|
||||
for name, data in deps_dict.items():
|
||||
if to_update is None or name in to_update:
|
||||
extracted_packages = integrate_package(name, data, cache_dir, out_dir)
|
||||
for pkg_name, version in extracted_packages:
|
||||
pubspec_overrides.append(f" {pkg_name}:\n path: {out_dir}/{pkg_name}\n")
|
||||
if version and version != "any":
|
||||
pubspec_deps.append(f" {pkg_name}: ^{version}\n")
|
||||
else:
|
||||
pubspec_deps.append(f" {pkg_name}: any\n")
|
||||
if "dependencies" in data:
|
||||
# If we updated the parent, we should update children? Or if no args provided, update all.
|
||||
# Actually, the original logic updated children automatically if parent is updated.
|
||||
process_deps_recursive(data["dependencies"], None if (to_update is None or name in to_update) else [])
|
||||
|
||||
process_deps_recursive(deps, repos_to_update if args.repo_name else None)
|
||||
|
||||
def sort_dependencies(d):
|
||||
sorted_d = {k: d[k] for k in sorted(d.keys())}
|
||||
for k, v in sorted_d.items():
|
||||
if "dependencies" in v and isinstance(v["dependencies"], dict):
|
||||
v["dependencies"] = sort_dependencies(v["dependencies"])
|
||||
return sorted_d
|
||||
|
||||
if "dependencies" in config:
|
||||
config["dependencies"] = sort_dependencies(config["dependencies"])
|
||||
|
||||
with open("dependencies.yaml", "w") as f:
|
||||
yaml.safe_dump(config, f, sort_keys=False)
|
||||
|
||||
# Update pubspec.yaml
|
||||
if not args.repo_name:
|
||||
with open("pubspec.yaml", "r") as f:
|
||||
pubspec_lines = f.readlines()
|
||||
|
||||
start_marker_overrides = "## --- Start Managed Dependency Overrides ---"
|
||||
end_marker_overrides = "## --- End Managed Dependency Overrides ---"
|
||||
|
||||
start_marker_deps = "## --- Start Managed Dependencies ---"
|
||||
end_marker_deps = "## --- End Managed Dependencies ---"
|
||||
|
||||
try:
|
||||
start_idx_overrides = next(i for i, line in enumerate(pubspec_lines) if line.strip() == start_marker_overrides)
|
||||
end_idx_overrides = next(i for i, line in enumerate(pubspec_lines) if line.strip() == end_marker_overrides)
|
||||
|
||||
# Update overrides section
|
||||
new_lines = pubspec_lines[:start_idx_overrides + 1] + pubspec_overrides + pubspec_lines[end_idx_overrides:]
|
||||
|
||||
# Now find the deps markers in the updated lines
|
||||
start_idx_deps = next(i for i, line in enumerate(new_lines) if line.strip() == start_marker_deps)
|
||||
end_idx_deps = next(i for i, line in enumerate(new_lines) if line.strip() == end_marker_deps)
|
||||
|
||||
# Update dependencies section
|
||||
final_lines = new_lines[:start_idx_deps + 1] + pubspec_deps + new_lines[end_idx_deps:]
|
||||
|
||||
with open("pubspec.yaml", "w") as f:
|
||||
f.writelines(final_lines)
|
||||
print_blue("Updated pubspec.yaml successfully.")
|
||||
except ValueError as e:
|
||||
print_yellow("Error: Could not find professional markers in pubspec.yaml.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
109
dependencies.yaml
Normal file
109
dependencies.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
cache: ./.cache
|
||||
outdir: ./dependencies
|
||||
dependencies:
|
||||
audio_waveforms:
|
||||
git: https://github.com/SimformSolutionsPvtLtd/audio_waveforms.git
|
||||
tag: 2.0.2
|
||||
avatar_maker:
|
||||
git: https://github.com/RoadTripMoustache/avatar_maker.git
|
||||
tag: 1.5.0
|
||||
keep:
|
||||
- assets/icons
|
||||
blurhash_dart:
|
||||
git: https://github.com/justacid/blurhash-dart.git
|
||||
tag: v1.2.1
|
||||
exif:
|
||||
git: https://github.com/bigflood/dartexif.git
|
||||
dependencies:
|
||||
sprintf:
|
||||
git: https://github.com/Naddiseo/dart-sprintf.git
|
||||
commit: f1e74f2f4c339d983f9d011b4ba1df4ec8b8857c
|
||||
commit: bf170d5639f0b6fcb0947060cf8bd7b623df9069
|
||||
flutter_blurhash:
|
||||
git: https://github.com/fluttercommunity/flutter_blurhash.git
|
||||
commit: c5cdec4986432e835bb91f5ce00564534450cdc7
|
||||
flutter_markdown_plus:
|
||||
git: https://github.com/foresightmobile/flutter_markdown_plus.git
|
||||
commit: dc1185c933fbf9dba559ef6c91586ff1503be3ee
|
||||
flutter_packages:
|
||||
git: https://github.com/otsmr/flutter-packages.git
|
||||
subpackages:
|
||||
- name: video_player
|
||||
path: packages/video_player/video_player
|
||||
- name: video_player_android
|
||||
path: packages/video_player/video_player_android
|
||||
- name: video_player_avfoundation
|
||||
path: packages/video_player/video_player_avfoundation
|
||||
- name: camera_android_camerax
|
||||
path: packages/camera/camera_android_camerax
|
||||
commit: b4f7d807a822a8ce327aae21353ac2217e928f6d
|
||||
flutter_sharing_intent:
|
||||
git: https://github.com/bhagat-techind/flutter_sharing_intent.git
|
||||
commit: aa1672f547d6579585fa27df0b28ffa2a2544aaa
|
||||
hand_signature:
|
||||
git: https://github.com/RomanBase/hand_signature.git
|
||||
commit: 1beedb164d093643365b0832277c377353c7464f
|
||||
hashlib:
|
||||
git: https://github.com/bitanon/hashlib.git
|
||||
replace:
|
||||
- - abstract class MACHashBase
|
||||
- abstract mixin class MACHashBase
|
||||
dependencies:
|
||||
hashlib_codecs:
|
||||
git: https://github.com/bitanon/hashlib_codecs.git
|
||||
commit: 2a966c37c3b9b1f5541ae88e99ab34acf3fc968b
|
||||
commit: bc9c2f8dd7bbc72f47ccab0ce1111d40259c49bc
|
||||
image:
|
||||
git: https://github.com/brendan-duncan/image.git
|
||||
tag: v4.9.2
|
||||
introduction_screen:
|
||||
git: https://github.com/Pyozer/introduction_screen.git
|
||||
dependencies:
|
||||
dots_indicator:
|
||||
git: https://github.com/Pyozer/dots_indicator.git
|
||||
commit: 508f5883ac79bdbc10254092de3f28f571d261cd
|
||||
commit: 4a90e557630b28834479ed9c64a9d2d0185d8e48
|
||||
libsignal_protocol_dart:
|
||||
git: https://github.com/MixinNetwork/libsignal_protocol_dart.git
|
||||
dependencies:
|
||||
adaptive_number:
|
||||
git: https://github.com/lemoony/adaptive_number_dart
|
||||
commit: ea9178fdd4d82ac45cf0ec966ac870dae661124f
|
||||
ed25519_edwards:
|
||||
git: https://github.com/Tougee/ed25519.git
|
||||
commit: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
|
||||
optional:
|
||||
git: https://github.com/tonio-ramirez/optional.dart.git
|
||||
commit: 71c638891ce4f2aff35c7387727989f31f9d877d
|
||||
pointycastle:
|
||||
git: https://github.com/bcgit/pc-dart.git
|
||||
commit: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
||||
x25519:
|
||||
git: https://github.com/Tougee/curve25519.git
|
||||
commit: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||
commit: c95a1586057022acdbb9c76b1692d94cc549bcc7
|
||||
lottie:
|
||||
git: https://github.com/xvrh/lottie-flutter.git
|
||||
commit: 127bc29f2c6bd8b32ec4064a09e54e6b31cd0a88
|
||||
mutex:
|
||||
git: https://github.com/hoylen/dart-mutex.git
|
||||
commit: 84ca903a3ac863735e3228c75a212133621f680f
|
||||
photo_view:
|
||||
git: https://github.com/bluefireteam/photo_view.git
|
||||
commit: a13ca2fc387a3fb1276126959e092c44d0029987
|
||||
pro_video_editor:
|
||||
git: https://github.com/hm21/pro_video_editor.git
|
||||
tag: v2.11.3
|
||||
qr_flutter:
|
||||
git: https://github.com/theyakka/qr.flutter.git
|
||||
dependencies:
|
||||
qr:
|
||||
git: https://github.com/kevmoo/qr.dart.git
|
||||
commit: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
||||
commit: d5e7206396105d643113618290bbcc755d05f492
|
||||
restart_app:
|
||||
git: https://github.com/gabrimatic/restart_app
|
||||
commit: 66897cb67e235bab85421647bfae036acb4438cb
|
||||
screen_protector:
|
||||
git: https://github.com/prongbang/screen_protector.git
|
||||
commit: 019c04d622d7b610d2903d3a347edc3ba76a6ed0
|
||||
|
|
@ -92,8 +92,6 @@ PODS:
|
|||
- nanopb/encode (3.30910.0)
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- pro_video_editor (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- rust_lib_twonly (0.0.1):
|
||||
- Flutter
|
||||
|
|
@ -122,7 +120,6 @@ DEPENDENCIES:
|
|||
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
|
||||
- rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
|
||||
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
|
||||
- SwiftProtobuf
|
||||
|
|
@ -170,8 +167,6 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
pro_video_editor:
|
||||
:path: ".symlinks/plugins/pro_video_editor/ios"
|
||||
rust_lib_twonly:
|
||||
:path: ".symlinks/plugins/rust_lib_twonly/ios"
|
||||
screen_protector:
|
||||
|
|
@ -182,7 +177,7 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
||||
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
||||
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||
|
|
@ -203,7 +198,6 @@ SPEC CHECKSUMS:
|
|||
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||
|
|
|
|||
|
|
@ -742,7 +742,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
@ -886,7 +886,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
|
@ -939,7 +939,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
|
|
@ -180,9 +181,17 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
}
|
||||
});
|
||||
|
||||
void handleShareMedia(String path, MediaType type) {
|
||||
HomeViewState.pendingSharedMedia = (path, type);
|
||||
routerProvider.go(Routes.home);
|
||||
HomeViewState.streamHomeViewPageIndex.add(0);
|
||||
HomeViewState.streamSharedMedia.add((path, type));
|
||||
}
|
||||
|
||||
_intentStreamSub = initIntentStreams(
|
||||
context,
|
||||
handleShareLink,
|
||||
handleShareMedia,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
),
|
||||
],
|
||||
)..where(groups.isDirectChat.equals(false)));
|
||||
return query.map((row) => row.readTable(groupMembers)).get();
|
||||
return await query.map((row) => row.readTable(groupMembers)).get();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return [];
|
||||
|
|
@ -346,8 +346,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
DateTime newLastMessage,
|
||||
) async {
|
||||
final now = clock.now();
|
||||
final clampedLastMessage =
|
||||
newLastMessage.isAfter(now) ? now : newLastMessage;
|
||||
final clampedLastMessage = newLastMessage.isAfter(now)
|
||||
? now
|
||||
: newLastMessage;
|
||||
await (update(groupMembers)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
await (delete(receivedReceipts)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
const Duration(days: 45),
|
||||
),
|
||||
)),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|||
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
||||
|
||||
@DataClassName('Message')
|
||||
@TableIndex(name: 'idx_messages_group_id_created_at', columns: {#groupId, #createdAt})
|
||||
class Messages extends Table {
|
||||
TextColumn get groupId =>
|
||||
text().references(Groups, #groupId, onDelete: KeyAction.cascade)();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
|||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
|
||||
@DataClassName('Receipt')
|
||||
@TableIndex(name: 'idx_receipts_message_id', columns: {#messageId})
|
||||
class Receipts extends Table {
|
||||
TextColumn get receiptId => text()();
|
||||
|
||||
|
|
|
|||
|
|
@ -13843,6 +13843,14 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
);
|
||||
late final $LabelsTable labels = $LabelsTable(this);
|
||||
late final $ContactLabelsTable contactLabels = $ContactLabelsTable(this);
|
||||
late final Index idxMessagesGroupIdCreatedAt = Index(
|
||||
'idx_messages_group_id_created_at',
|
||||
'CREATE INDEX idx_messages_group_id_created_at ON messages (group_id, created_at)',
|
||||
);
|
||||
late final Index idxReceiptsMessageId = Index(
|
||||
'idx_receipts_message_id',
|
||||
'CREATE INDEX idx_receipts_message_id ON receipts (message_id)',
|
||||
);
|
||||
late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB);
|
||||
late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB);
|
||||
late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB);
|
||||
|
|
@ -13889,6 +13897,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
shortcutMembers,
|
||||
labels,
|
||||
contactLabels,
|
||||
idxMessagesGroupIdCreatedAt,
|
||||
idxReceiptsMessageId,
|
||||
];
|
||||
@override
|
||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
|
||||
|
|
|
|||
|
|
@ -1601,7 +1601,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @backupSelectStrongPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose a secure password. This is required if you want to restore your backup.'**
|
||||
/// **'Select a secure password. It is recommended to generate and store it via your password manager of choice.'**
|
||||
String get backupSelectStrongPassword;
|
||||
|
||||
/// No description provided for @password.
|
||||
|
|
@ -1631,7 +1631,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @backupEnableBackup.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Activate automatic backup'**
|
||||
/// **'Enable Password Backup'**
|
||||
String get backupEnableBackup;
|
||||
|
||||
/// No description provided for @backupTwonlySaveNow.
|
||||
|
|
@ -3713,7 +3713,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password Recovery'**
|
||||
/// **'Trusted Friends'**
|
||||
String get passwordlessRecovery;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNotConfigured.
|
||||
|
|
@ -3833,7 +3833,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecoveryEnableSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Passwordless recovery successfully enabled!'**
|
||||
/// **'Trusted Friends successfully set up!'**
|
||||
String get passwordlessRecoveryEnableSuccess;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnterPin.
|
||||
|
|
@ -3851,19 +3851,19 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecoveryEnableBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable Passwordless Recovery'**
|
||||
/// **'Enable Trusted Friends'**
|
||||
String get passwordlessRecoveryEnableBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryRecoverBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recover passwordless'**
|
||||
/// **'Recover via Trusted Friends'**
|
||||
String get passwordlessRecoveryRecoverBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryModifyBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modify Passwordless Recovery'**
|
||||
/// **'Modify Trusted Friends'**
|
||||
String get passwordlessRecoveryModifyBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryStatusEnabled.
|
||||
|
|
@ -4435,6 +4435,108 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Dark'**
|
||||
String get themeDark;
|
||||
|
||||
/// No description provided for @backupRecoverySectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Secure Account Access'**
|
||||
String get backupRecoverySectionTitle;
|
||||
|
||||
/// No description provided for @backupRecoverySectionDescNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.'**
|
||||
String get backupRecoverySectionDescNone;
|
||||
|
||||
/// No description provided for @backupRecoverySectionDescSome.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can recover your account with the recovery options below. It is recommended to enable both options.'**
|
||||
String get backupRecoverySectionDescSome;
|
||||
|
||||
/// No description provided for @backupRecoveryOptionAFriends.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Via trusted friends'**
|
||||
String get backupRecoveryOptionAFriends;
|
||||
|
||||
/// No description provided for @backupRecoveryOptionAMicrocopy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select friends who can help you recover your account.'**
|
||||
String get backupRecoveryOptionAMicrocopy;
|
||||
|
||||
/// No description provided for @backupRecoveryOptionBPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Classic Password'**
|
||||
String get backupRecoveryOptionBPassword;
|
||||
|
||||
/// No description provided for @backupRecoveryOptionBMicrocopy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set a password for your access.'**
|
||||
String get backupRecoveryOptionBMicrocopy;
|
||||
|
||||
/// No description provided for @backupCloudSectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup Your Data'**
|
||||
String get backupCloudSectionTitle;
|
||||
|
||||
/// No description provided for @backupCloudSectionDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup your content so nothing gets lost.'**
|
||||
String get backupCloudSectionDesc;
|
||||
|
||||
/// No description provided for @backupCloudContactsMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contacts & Messages'**
|
||||
String get backupCloudContactsMessages;
|
||||
|
||||
/// No description provided for @backupCloudFreeActive.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Free Active'**
|
||||
String get backupCloudFreeActive;
|
||||
|
||||
/// No description provided for @backupCloudImagesMedia.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Images & Media'**
|
||||
String get backupCloudImagesMedia;
|
||||
|
||||
/// No description provided for @backupCloudProBadge.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pro'**
|
||||
String get backupCloudProBadge;
|
||||
|
||||
/// No description provided for @memoriesAddingToFavorites.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Adding to favorites...'**
|
||||
String get memoriesAddingToFavorites;
|
||||
|
||||
/// No description provided for @memoriesRemovingFromFavorites.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Removing from favorites...'**
|
||||
String get memoriesRemovingFromFavorites;
|
||||
|
||||
/// No description provided for @memoriesDeletingProgress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Deleting memories...'**
|
||||
String get memoriesDeletingProgress;
|
||||
|
||||
/// No description provided for @memoriesExportingProgress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Exporting memories...'**
|
||||
String get memoriesExportingProgress;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -850,7 +850,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupSelectStrongPassword =>
|
||||
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.';
|
||||
'Wähle ein sicheres Passwort. Es wird empfohlen, dieses über deinen bevorzugten Passwort-Manager zu generieren und zu speichern.';
|
||||
|
||||
@override
|
||||
String get password => 'Passwort';
|
||||
|
|
@ -866,7 +866,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||
|
||||
@override
|
||||
String get backupEnableBackup => 'Automatische Sicherung aktivieren';
|
||||
String get backupEnableBackup => 'Passwort-Backup aktivieren';
|
||||
|
||||
@override
|
||||
String get backupTwonlySaveNow => 'Jetzt speichern';
|
||||
|
|
@ -2144,7 +2144,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Passwort vergessen';
|
||||
String get passwordlessRecovery => 'Vertraute Freunde';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
||||
|
|
@ -2210,7 +2210,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'\"Passwort vergessen\" erfolgreich aktiviert!';
|
||||
'Vertraute Freunde erfolgreich eingerichtet!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
||||
|
|
@ -2220,16 +2220,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Bitte gib eine E-Mail-Adresse ein.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn =>
|
||||
'\"Passwort vergessen\" aktivieren';
|
||||
String get passwordlessRecoveryEnableBtn => 'Vertraute Freunde aktivieren';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn =>
|
||||
'Mit \"Passwort vergessen\" wiederherstellen';
|
||||
'Mithilfe von vertrauten Freunden wiederherstellen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn =>
|
||||
'\"Passwort vergessen\" bearbeiten';
|
||||
String get passwordlessRecoveryModifyBtn => 'Vertraute Freunde bearbeiten';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
|
|
@ -2568,4 +2566,60 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get themeDark => 'Dunkel';
|
||||
|
||||
@override
|
||||
String get backupRecoverySectionTitle => 'Konto-Zugang sichern';
|
||||
|
||||
@override
|
||||
String get backupRecoverySectionDescNone =>
|
||||
'Du musst mindestens eine Wiederherstellungsoption auswählen, da dir sonst niemand helfen kann, falls du den Zugriff auf dein Gerät verlierst.';
|
||||
|
||||
@override
|
||||
String get backupRecoverySectionDescSome =>
|
||||
'Mit den untenstehenden Wiederherstellungsoptionen kannst du dein Konto wiederherstellen. Es wird empfohlen, beide Optionen zu aktivieren.';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionAFriends => 'Über vertraute Freunde';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionAMicrocopy =>
|
||||
'Wähle Freunde aus, die dir helfen, dein Konto wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionBPassword => 'Klassisches Passwort';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionBMicrocopy =>
|
||||
'Lege ein Passwort für deinen Zugang fest.';
|
||||
|
||||
@override
|
||||
String get backupCloudSectionTitle => 'Deine Daten sichern';
|
||||
|
||||
@override
|
||||
String get backupCloudSectionDesc =>
|
||||
'Sichere deine Inhalte, damit nichts verloren geht.';
|
||||
|
||||
@override
|
||||
String get backupCloudContactsMessages => 'Kontakte & Nachrichten';
|
||||
|
||||
@override
|
||||
String get backupCloudFreeActive => 'Kostenlos aktiv';
|
||||
|
||||
@override
|
||||
String get backupCloudImagesMedia => 'Bilder & Medien';
|
||||
|
||||
@override
|
||||
String get backupCloudProBadge => 'Pro';
|
||||
|
||||
@override
|
||||
String get memoriesAddingToFavorites => 'Zu Favoriten hinzufügen...';
|
||||
|
||||
@override
|
||||
String get memoriesRemovingFromFavorites => 'Aus Favoriten entfernen...';
|
||||
|
||||
@override
|
||||
String get memoriesDeletingProgress => 'Erinnerungen werden gelöscht...';
|
||||
|
||||
@override
|
||||
String get memoriesExportingProgress => 'Erinnerungen werden exportiert...';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -845,7 +845,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupSelectStrongPassword =>
|
||||
'Choose a secure password. This is required if you want to restore your backup.';
|
||||
'Select a secure password. It is recommended to generate and store it via your password manager of choice.';
|
||||
|
||||
@override
|
||||
String get password => 'Password';
|
||||
|
|
@ -861,7 +861,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'Password must be at least 10 characters long.';
|
||||
|
||||
@override
|
||||
String get backupEnableBackup => 'Activate automatic backup';
|
||||
String get backupEnableBackup => 'Enable Password Backup';
|
||||
|
||||
@override
|
||||
String get backupTwonlySaveNow => 'Save now';
|
||||
|
|
@ -2129,7 +2129,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get avatarCustomizeReset => 'Reset';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Password Recovery';
|
||||
String get passwordlessRecovery => 'Trusted Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
||||
|
|
@ -2195,7 +2195,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'Passwordless recovery successfully enabled!';
|
||||
'Trusted Friends successfully set up!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Please enter a PIN.';
|
||||
|
|
@ -2204,13 +2204,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get passwordlessRecoveryEnterEmail => 'Please enter an email address.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery';
|
||||
String get passwordlessRecoveryEnableBtn => 'Enable Trusted Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn => 'Recover passwordless';
|
||||
String get passwordlessRecoveryRecoverBtn => 'Recover via Trusted Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery';
|
||||
String get passwordlessRecoveryModifyBtn => 'Modify Trusted Friends';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
|
|
@ -2541,4 +2541,60 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get themeDark => 'Dark';
|
||||
|
||||
@override
|
||||
String get backupRecoverySectionTitle => 'Secure Account Access';
|
||||
|
||||
@override
|
||||
String get backupRecoverySectionDescNone =>
|
||||
'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.';
|
||||
|
||||
@override
|
||||
String get backupRecoverySectionDescSome =>
|
||||
'You can recover your account with the recovery options below. It is recommended to enable both options.';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionAFriends => 'Via trusted friends';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionAMicrocopy =>
|
||||
'Select friends who can help you recover your account.';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionBPassword => 'Classic Password';
|
||||
|
||||
@override
|
||||
String get backupRecoveryOptionBMicrocopy =>
|
||||
'Set a password for your access.';
|
||||
|
||||
@override
|
||||
String get backupCloudSectionTitle => 'Backup Your Data';
|
||||
|
||||
@override
|
||||
String get backupCloudSectionDesc =>
|
||||
'Backup your content so nothing gets lost.';
|
||||
|
||||
@override
|
||||
String get backupCloudContactsMessages => 'Contacts & Messages';
|
||||
|
||||
@override
|
||||
String get backupCloudFreeActive => 'Free Active';
|
||||
|
||||
@override
|
||||
String get backupCloudImagesMedia => 'Images & Media';
|
||||
|
||||
@override
|
||||
String get backupCloudProBadge => 'Pro';
|
||||
|
||||
@override
|
||||
String get memoriesAddingToFavorites => 'Adding to favorites...';
|
||||
|
||||
@override
|
||||
String get memoriesRemovingFromFavorites => 'Removing from favorites...';
|
||||
|
||||
@override
|
||||
String get memoriesDeletingProgress => 'Deleting memories...';
|
||||
|
||||
@override
|
||||
String get memoriesExportingProgress => 'Exporting memories...';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5
|
||||
Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486
|
||||
|
|
@ -23,7 +23,6 @@ class UserData {
|
|||
String username;
|
||||
String displayName;
|
||||
String? avatarSvg;
|
||||
String? avatarJson;
|
||||
|
||||
@JsonKey(defaultValue: 0)
|
||||
int appVersion = 0;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
|
||||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||
|
|
@ -129,7 +128,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'username': instance.username,
|
||||
'displayName': instance.displayName,
|
||||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'appVersion': instance.appVersion,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'isDeveloper': instance.isDeveloper,
|
||||
|
|
|
|||
|
|
@ -227,11 +227,19 @@ Future<void> finishStartedPreprocessing() async {
|
|||
mediaFile.mediaId,
|
||||
);
|
||||
if (messages.isEmpty) {
|
||||
Log.info(
|
||||
'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.',
|
||||
);
|
||||
MediaFileService(mediaFile).fullMediaRemoval();
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||
if (mediaFile.createdAt.isBefore(
|
||||
clock.now().subtract(const Duration(hours: 1)),
|
||||
)) {
|
||||
Log.info(
|
||||
'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.',
|
||||
);
|
||||
MediaFileService(mediaFile).fullMediaRemoval();
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||
} else {
|
||||
Log.info(
|
||||
'Media file ${mediaFile.mediaId} has no messages, but is too new to be deleted by finishStartedPreprocessing. Skipping.',
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -484,12 +492,20 @@ Future<void> _startBackgroundMediaUploadInternal(
|
|||
mediaService.mediaFile.mediaId,
|
||||
);
|
||||
if (messages.isEmpty) {
|
||||
Log.warn(
|
||||
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.',
|
||||
);
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(
|
||||
mediaService.mediaFile.mediaId,
|
||||
);
|
||||
if (mediaService.mediaFile.createdAt.isBefore(
|
||||
clock.now().subtract(const Duration(hours: 1)),
|
||||
)) {
|
||||
Log.warn(
|
||||
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.',
|
||||
);
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(
|
||||
mediaService.mediaFile.mediaId,
|
||||
);
|
||||
} else {
|
||||
Log.warn(
|
||||
'Media files ${mediaService.mediaFile.mediaId} has no paths, but is too new to be deleted. Skipping deletion.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.warn(
|
||||
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path, but messages still reference it. Marking as uploaded to stop retries.',
|
||||
|
|
|
|||
|
|
@ -114,22 +114,6 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
// ignore: parameter_assignments
|
||||
receipt = loadedReceipt;
|
||||
|
||||
if (receipt.retryCount >= 2) {
|
||||
// After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received,
|
||||
// but the app was closed before the message was fully processed. Because the receipt was already stored, subsequent retries were
|
||||
// detected as duplicates and rejected.
|
||||
final oldReceiptId = receipt.receiptId;
|
||||
final updatedReceipt = await twonlyDB.receiptsDao.rotateReceiptId(
|
||||
oldReceiptId,
|
||||
);
|
||||
if (updatedReceipt != null) {
|
||||
Log.info(
|
||||
'Changed receiptId $oldReceiptId to ${updatedReceipt.receiptId} as retryCount is ${receipt.retryCount}',
|
||||
);
|
||||
receipt = updatedReceipt;
|
||||
}
|
||||
}
|
||||
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
receipt.contactId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -141,6 +141,26 @@ Future<void> _handleClient2ClientMessage(
|
|||
}
|
||||
|
||||
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
|
||||
Log.info(
|
||||
'[$receiptId] Message is a duplicate. Sending delivery receipt again.',
|
||||
);
|
||||
try {
|
||||
final response = Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
|
||||
await twonlyDB.receiptsDao.insertReceipt(
|
||||
ReceiptsCompanion(
|
||||
receiptId: Value(receiptId),
|
||||
contactId: Value(fromUserId),
|
||||
message: Value(response.writeToBuffer()),
|
||||
contactWillSendsReceipt: const Value(false),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e');
|
||||
}
|
||||
await tryToSendCompleteMessage(
|
||||
receiptId: receiptId,
|
||||
blocking: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -269,12 +289,13 @@ Future<void> _handleClient2ClientMessage(
|
|||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error inserting receipt: $e');
|
||||
}
|
||||
if (targetReceiptId != null) {
|
||||
await tryToSendCompleteMessage(
|
||||
receiptId: targetReceiptId,
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
|
||||
targetReceiptId ??= receiptIdDB?.value ?? receiptId;
|
||||
|
||||
await tryToSendCompleteMessage(
|
||||
receiptId: targetReceiptId,
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
case Message_Type.TEST_NOTIFICATION:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
}
|
||||
// the state is now updated and the appended_group_state should be removed on the server, so just call this
|
||||
// function again, to sync the local database
|
||||
return fetchGroupState(group);
|
||||
return await fetchGroupState(group);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -12,7 +10,6 @@ import 'package:twonly/locator.dart';
|
|||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/passwordless_recovery.service.dart'
|
||||
show PasswordlessRecoveryService;
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
|
|
@ -20,7 +17,6 @@ import 'package:twonly/src/utils/log.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart';
|
||||
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
||||
|
||||
|
|
@ -149,53 +145,20 @@ Future<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> handleIntentMediaFile(
|
||||
BuildContext context,
|
||||
String filePath,
|
||||
MediaType type,
|
||||
) async {
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
Log.error('The shared intent file does not exits.');
|
||||
return;
|
||||
}
|
||||
|
||||
final newMediaService = await initializeMediaUpload(
|
||||
type,
|
||||
userService.currentUser.defaultShowTime,
|
||||
);
|
||||
if (newMediaService == null) {
|
||||
Log.error('Could not create new media file for intent shared file');
|
||||
return;
|
||||
}
|
||||
|
||||
file.copySync(newMediaService.originalPath.path);
|
||||
if (!context.mounted) return;
|
||||
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageEditorView(
|
||||
mediaFileService: newMediaService,
|
||||
sharedFromGallery: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
StreamSubscription<List<SharedFile>> initIntentStreams(
|
||||
BuildContext context,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
void Function(String, MediaType) onMediaCallBack,
|
||||
) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
||||
if (!context.mounted) return;
|
||||
handleIntentSharedFile(context, f, onUrlCallBack);
|
||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
});
|
||||
|
||||
return FlutterSharingIntent.instance.getMediaStream().listen(
|
||||
(f) {
|
||||
if (!context.mounted) return;
|
||||
handleIntentSharedFile(context, f, onUrlCallBack);
|
||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
},
|
||||
// ignore: inference_failure_on_untyped_parameter
|
||||
onError: (err) {
|
||||
|
|
@ -208,6 +171,7 @@ Future<void> handleIntentSharedFile(
|
|||
BuildContext context,
|
||||
List<SharedFile> files,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
void Function(String, MediaType) onMediaCallBack,
|
||||
) async {
|
||||
for (final file in files) {
|
||||
if (file.value == null) {
|
||||
|
|
@ -231,9 +195,9 @@ Future<void> handleIntentSharedFile(
|
|||
if (file.value!.endsWith('.gif')) {
|
||||
type = MediaType.gif;
|
||||
}
|
||||
await handleIntentMediaFile(context, file.value!, type);
|
||||
onMediaCallBack(file.value!, type);
|
||||
case SharedMediaType.VIDEO:
|
||||
await handleIntentMediaFile(context, file.value!, MediaType.video);
|
||||
onMediaCallBack(file.value!, MediaType.video);
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,11 +202,11 @@ class PasswordlessRecoveryService {
|
|||
case SecondFactorType.pin:
|
||||
|
||||
// The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin
|
||||
config.serverKeyProtection = getRandomUint8List(32);
|
||||
|
||||
// As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the
|
||||
// unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion.
|
||||
config.pinUnlockToken = getRandomUint8List(32);
|
||||
config
|
||||
..serverKeyProtection = getRandomUint8List(32)
|
||||
// As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the
|
||||
// unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion.
|
||||
..pinUnlockToken = getRandomUint8List(32);
|
||||
|
||||
// Brute-force protection for the user's pin:
|
||||
// - Server: Does not know the seed.
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ signalDecryptMessageV1(
|
|||
) = await lockingSignalProtocol.protect(() async {
|
||||
Log.info('Lock acquired for $fromUserId (V1)');
|
||||
try {
|
||||
// Yield execution to the event loop to prevent UI freezing during bulk decryption
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
final session = SessionCipher.fromStore(
|
||||
(await getSignalStore())!,
|
||||
getSignalAddress(fromUserId),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'dart:ui' as ui;
|
|||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -114,29 +115,33 @@ File currentUserAvatarFile(int avatarCounter) {
|
|||
return File('${avatarsDirectory.path}/user_$avatarCounter.png');
|
||||
}
|
||||
|
||||
final _avatarMutex = Mutex();
|
||||
|
||||
Future<String?> getUserAvatar() async {
|
||||
if (userService.currentUser.avatarSvg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final avatarCounter = userService.currentUser.avatarCounter;
|
||||
final file = currentUserAvatarFile(avatarCounter);
|
||||
if (file.existsSync()) {
|
||||
return _avatarMutex.protect(() async {
|
||||
final avatarCounter = userService.currentUser.avatarCounter;
|
||||
final file = currentUserAvatarFile(avatarCounter);
|
||||
if (file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
|
||||
final pictureInfo = await vg.loadPicture(
|
||||
SvgStringLoader(userService.currentUser.avatarSvg!),
|
||||
null,
|
||||
);
|
||||
|
||||
final image = await pictureInfo.picture.toImage(270, 300);
|
||||
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
await file.writeAsBytes(pngBytes, flush: true);
|
||||
pictureInfo.picture.dispose();
|
||||
|
||||
return file.path;
|
||||
}
|
||||
|
||||
final pictureInfo = await vg.loadPicture(
|
||||
SvgStringLoader(userService.currentUser.avatarSvg!),
|
||||
null,
|
||||
);
|
||||
|
||||
final image = await pictureInfo.picture.toImage(270, 300);
|
||||
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
await file.writeAsBytes(pngBytes);
|
||||
pictureInfo.picture.dispose();
|
||||
|
||||
return file.path;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
|
|
@ -6,7 +7,6 @@ import 'package:convert/convert.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -44,41 +44,41 @@ Future<String?> saveImageToGallery(
|
|||
|
||||
if (createdAt != null) {
|
||||
try {
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image != null) {
|
||||
final formattedDate = DateFormat(
|
||||
'yyyy:MM:dd HH:mm:ss',
|
||||
).format(createdAt);
|
||||
image.exif.imageIfd[0x0132] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTime
|
||||
image.exif.exifIfd[0x9003] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeOriginal
|
||||
image.exif.exifIfd[0x9004] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeDigitized
|
||||
bytesToProcess = await Isolate.run(() {
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image != null) {
|
||||
final formattedDate = DateFormat(
|
||||
'yyyy:MM:dd HH:mm:ss',
|
||||
).format(createdAt);
|
||||
image.exif.imageIfd[0x0132] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTime
|
||||
image.exif.exifIfd[0x9003] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeOriginal
|
||||
image.exif.exifIfd[0x9004] = img.IfdValueAscii(
|
||||
formattedDate,
|
||||
); // DateTimeDigitized
|
||||
|
||||
bytesToProcess = img.encodeJpg(image);
|
||||
}
|
||||
return img.encodeJpg(image);
|
||||
}
|
||||
return imageBytes;
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final jpgImages = await FlutterImageCompress.compressWithList(
|
||||
// ignore: avoid_redundant_argument_values
|
||||
format: CompressFormat.jpeg,
|
||||
bytesToProcess,
|
||||
quality: 100,
|
||||
keepExif: true,
|
||||
);
|
||||
final hasAccess = await Gal.hasAccess(toAlbum: true);
|
||||
if (!hasAccess) {
|
||||
await Gal.requestAccess(toAlbum: true);
|
||||
}
|
||||
try {
|
||||
await Gal.putImageBytes(jpgImages, album: 'twonly', name: name ?? 'image');
|
||||
await Gal.putImageBytes(
|
||||
bytesToProcess,
|
||||
album: 'twonly',
|
||||
name: name ?? 'image',
|
||||
);
|
||||
return null;
|
||||
} on GalException catch (e) {
|
||||
Log.error(e);
|
||||
|
|
|
|||
148
lib/src/visual/components/cached_network_image.dart
Normal file
148
lib/src/visual/components/cached_network_image.dart
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class CachedNetworkImage extends StatefulWidget {
|
||||
const CachedNetworkImage({
|
||||
required this.imageUrl,
|
||||
super.key,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.placeholder,
|
||||
this.errorWidget,
|
||||
});
|
||||
final String imageUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit? fit;
|
||||
final Widget Function(BuildContext, String)? placeholder;
|
||||
final Widget Function(BuildContext, String, dynamic)? errorWidget;
|
||||
|
||||
@override
|
||||
State<CachedNetworkImage> createState() => _CachedNetworkImageState();
|
||||
}
|
||||
|
||||
class _CachedNetworkImageState extends State<CachedNetworkImage> {
|
||||
File? _imageFile;
|
||||
bool _isLoading = true;
|
||||
dynamic _error;
|
||||
static bool _hasCleanedUp = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CachedNetworkImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.imageUrl != widget.imageUrl) {
|
||||
_loadImage();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadImage() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final cacheDir = await getTemporaryDirectory();
|
||||
final urlHash = md5.convert(utf8.encode(widget.imageUrl)).toString();
|
||||
final file = File('${cacheDir.path}/custom_cached_image_$urlHash');
|
||||
|
||||
if (!_hasCleanedUp) {
|
||||
_hasCleanedUp = true;
|
||||
unawaited(_cleanupOldFiles(cacheDir)); // Run asynchronously
|
||||
}
|
||||
|
||||
if (file.existsSync()) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imageFile = file;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final client = HttpClient();
|
||||
final request = await client.getUrl(Uri.parse(widget.imageUrl));
|
||||
final response = await request.close();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
await response.pipe(file.openWrite());
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imageFile = file;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw Exception('Failed to load image: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = e;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanupOldFiles(Directory cacheDir) async {
|
||||
try {
|
||||
final files = cacheDir.listSync();
|
||||
final now = DateTime.now();
|
||||
for (final f in files) {
|
||||
if (f is File && f.path.contains('custom_cached_image_')) {
|
||||
final stat = f.statSync();
|
||||
if (now.difference(stat.modified).inDays >= 7) {
|
||||
await f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
if (widget.placeholder != null) {
|
||||
return widget.placeholder!(context, widget.imageUrl);
|
||||
}
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
|
||||
if (_error != null || _imageFile == null) {
|
||||
if (widget.errorWidget != null) {
|
||||
return widget.errorWidget!(context, widget.imageUrl, _error);
|
||||
}
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
_imageFile!,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
if (widget.errorWidget != null) {
|
||||
return widget.errorWidget!(context, widget.imageUrl, error);
|
||||
}
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,9 +24,7 @@ class _VideoPlayerFileHelperState extends State<VideoPlayerFileHelper> {
|
|||
super.initState();
|
||||
_controller = VideoPlayerController.file(
|
||||
widget.videoPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
videoPlayerOptions: VideoPlayerOptions(),
|
||||
);
|
||||
|
||||
unawaited(
|
||||
|
|
|
|||
|
|
@ -780,10 +780,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
mc.sharedLinkForPreview != null &&
|
||||
mc.sharedLinkForPreview!.shouldGeneratePreview &&
|
||||
!mc.isVideoRecording)
|
||||
ShowTitleText(
|
||||
title: mc.sharedLinkForPreview!.url.host,
|
||||
desc: 'Link',
|
||||
isLink: true,
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Chip(
|
||||
label: Text(mc.sharedLinkForPreview!.url.host),
|
||||
onDeleted: () {
|
||||
mc.setSharedLinkForPreview(null);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!mc.isSharePreviewIsShown &&
|
||||
!mc.isVideoRecording &&
|
||||
|
|
|
|||
|
|
@ -91,10 +91,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
),
|
||||
)
|
||||
else if (_imageSaved)
|
||||
const Icon(Icons.check, size: 14)
|
||||
else
|
||||
_imageSaved
|
||||
? const Icon(Icons.check, size: 14)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 14),
|
||||
const FaIcon(FontAwesomeIcons.floppyDisk, size: 14),
|
||||
if (widget.displayButtonLabel) const SizedBox(width: 10),
|
||||
if (widget.displayButtonLabel)
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -116,9 +116,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
});
|
||||
videoController = VideoPlayerController.file(
|
||||
mediaService.originalPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
videoPlayerOptions: VideoPlayerOptions(),
|
||||
);
|
||||
videoController?.setLooping(true);
|
||||
videoController
|
||||
|
|
@ -127,7 +125,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
await videoController!.play();
|
||||
setState(() {});
|
||||
})
|
||||
// ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error
|
||||
// ignore: argument_type_not_assignable_to_error_handler
|
||||
.catchError(Log.error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart';
|
||||
|
||||
class ImageFilter extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||
|
||||
class CustomLinkCard extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
// Assuming the same Metadata import structure
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||
|
||||
class YouTubePostCard extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
contactSub?.cancel();
|
||||
groupActionsSub?.cancel();
|
||||
_nextTypingIndicator?.cancel();
|
||||
try {
|
||||
textFieldFocus?.dispose();
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart';
|
||||
|
|
@ -202,7 +203,22 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: right
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (info.displayUserName != '' && !widget.group.isDirectChat)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2, left: 4),
|
||||
child: Text(
|
||||
info.displayUserName,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
ResponseContainer(
|
||||
msg: widget.message,
|
||||
group: widget.group,
|
||||
|
|
@ -256,17 +272,18 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!right && !widget.group.isDirectChat)
|
||||
hideContactAvatar
|
||||
? const SizedBox(width: 24)
|
||||
: GestureDetector(
|
||||
onTap: () => context.push(
|
||||
Routes.profileContact(widget.message.senderId!),
|
||||
),
|
||||
child: AvatarIcon(
|
||||
contactId: widget.message.senderId,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (hideContactAvatar)
|
||||
const SizedBox(width: 24)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () => context.push(
|
||||
Routes.profileContact(widget.message.senderId!),
|
||||
),
|
||||
child: AvatarIcon(
|
||||
contactId: widget.message.senderId,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -51,18 +51,6 @@ class ChatTextEntry extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (info.displayUserName != '')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
info.displayUserName,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import 'package:clock/clock.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||
|
||||
class FriendlyMessageTime extends StatelessWidget {
|
||||
const FriendlyMessageTime({
|
||||
|
|
@ -17,6 +20,8 @@ class FriendlyMessageTime extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statusIcon = _buildStatusIcon(Colors.grey.shade400);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Row(
|
||||
|
|
@ -48,10 +53,66 @@ class FriendlyMessageTime extends StatelessWidget {
|
|||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
?statusIcon,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildStatusIcon(Color iconColor) {
|
||||
if (message.type != MessageType.text.name || message.senderId != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (message.ackByServer == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: ThreeRotatingDots(size: 8, color: iconColor),
|
||||
);
|
||||
}
|
||||
|
||||
if (message.openedByAll != null || message.openedAt != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.solidEye,
|
||||
size: 8,
|
||||
color: iconColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Now check message actions for ackByUserAt
|
||||
return StreamBuilder<List<(MessageAction, Contact)>>(
|
||||
stream: twonlyDB.messagesDao.watchMessageActions(message.messageId),
|
||||
builder: (context, snapshot) {
|
||||
final actions = snapshot.data ?? [];
|
||||
final hasAckByUser = actions.any(
|
||||
(t) => t.$1.type == MessageActionType.ackByUserAt,
|
||||
);
|
||||
|
||||
if (hasAckByUser) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.checkDouble,
|
||||
size: 8,
|
||||
color: iconColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.check,
|
||||
size: 8,
|
||||
color: iconColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String friendlyTime(BuildContext context, DateTime dt) {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
|||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
||||
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
|
||||
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -426,9 +426,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
|
||||
final controller = VideoPlayerController.file(
|
||||
mediaLocal.tempPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: mediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||
),
|
||||
videoPlayerOptions: VideoPlayerOptions(),
|
||||
);
|
||||
|
||||
await controller.setLooping(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -9,7 +10,9 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -47,10 +50,14 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
||||
StreamSubscription<int>? _homeViewPageIndexSub;
|
||||
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
||||
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
|
||||
|
||||
static Uri? pendingSharedLink;
|
||||
static (String, MediaType)? pendingSharedMedia;
|
||||
static final streamHomeViewPageIndex = StreamController<int>.broadcast();
|
||||
static final streamSharedLink = StreamController<Uri>.broadcast();
|
||||
static final streamSharedMedia =
|
||||
StreamController<(String, MediaType)>.broadcast();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -59,6 +66,8 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
var initialPage = widget.initialPage;
|
||||
if (HomeViewState.pendingSharedLink != null) {
|
||||
initialPage = 1;
|
||||
} else if (HomeViewState.pendingSharedMedia != null) {
|
||||
initialPage = 0;
|
||||
} else if (initialPage == 1 &&
|
||||
!userService.currentUser.startWithCameraOpen) {
|
||||
initialPage = 0;
|
||||
|
|
@ -114,12 +123,48 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
});
|
||||
});
|
||||
|
||||
_sharedMediaSub = streamSharedMedia.stream.listen((media) async {
|
||||
HomeViewState.pendingSharedMedia = null;
|
||||
final type = media.$2;
|
||||
final filePath = media.$1;
|
||||
|
||||
final newMediaService = await initializeMediaUpload(
|
||||
type,
|
||||
userService.currentUser.defaultShowTime,
|
||||
);
|
||||
if (newMediaService == null) {
|
||||
Log.error('Could not create new media file for intent shared file');
|
||||
return;
|
||||
}
|
||||
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
Log.error('The shared intent file does not exist.');
|
||||
return;
|
||||
}
|
||||
file.copySync(newMediaService.originalPath.path);
|
||||
if (!mounted) return;
|
||||
|
||||
await context.navPush(
|
||||
ShareImageEditorView(
|
||||
mediaFileService: newMediaService,
|
||||
sharedFromGallery: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
if (HomeViewState.pendingSharedLink != null) {
|
||||
final link = HomeViewState.pendingSharedLink!;
|
||||
HomeViewState.pendingSharedLink = null;
|
||||
_mainCameraController.setSharedLinkForPreview(link);
|
||||
}
|
||||
|
||||
if (HomeViewState.pendingSharedMedia != null) {
|
||||
final media = HomeViewState.pendingSharedMedia!;
|
||||
HomeViewState.pendingSharedMedia = null;
|
||||
streamSharedMedia.add(media);
|
||||
}
|
||||
|
||||
if (initialPage == 1) {
|
||||
Permission.camera.isGranted.then((hasPermission) {
|
||||
if (hasPermission && mounted) {
|
||||
|
|
@ -204,6 +249,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
_mainCameraController.setState = null;
|
||||
_mainCameraController.closeCamera();
|
||||
_sharedLinkSub?.cancel();
|
||||
_sharedMediaSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
|
@ -11,6 +18,9 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFreePlan =
|
||||
context.watch<PurchasesProvider>().plan == SubscriptionPlan.Free;
|
||||
|
||||
return StreamBuilder<void>(
|
||||
stream: userService.onUserUpdated,
|
||||
builder: (context, snapshot) {
|
||||
|
|
@ -24,55 +34,99 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.color.primaryContainer.withValues(alpha: 0.15),
|
||||
context.color.primaryContainer.withValues(alpha: 0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
color: context.color.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: context.color.primary.withValues(alpha: 0.1),
|
||||
width: 1.5,
|
||||
color: context.color.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.memoriesBackupTitle,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: context.color.onSurface,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.lang.memoriesBackupTitle,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: context.color.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade700,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.star,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.lang.backupCloudProBadge,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
context.lang.settingsStorageNoCloudBackupCard,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.color.onSurfaceVariant,
|
||||
height: 1.3,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: () async {
|
||||
if (isFreePlan) {
|
||||
unawaited(
|
||||
context.push(Routes.settingsSubscription),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = true,
|
||||
);
|
||||
|
|
@ -80,9 +134,7 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
},
|
||||
child: Text(context.lang.enable),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await UserService.update(
|
||||
(u) => u.hideMemoriesBackupPromo = true,
|
||||
|
|
@ -90,6 +142,10 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
|
|||
},
|
||||
child: Text(
|
||||
context.lang.settingsStorageHidePromo,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -306,11 +306,11 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
if (deleteCompletely == null) return;
|
||||
if (deleteCompletely == null || !mounted) return;
|
||||
final isCompletely = deleteCompletely;
|
||||
|
||||
await _showProgressDialog(
|
||||
'Deleting memories...',
|
||||
context.lang.memoriesDeletingProgress,
|
||||
(setProgress) async {
|
||||
for (var i = 0; i < selectedList.length; i++) {
|
||||
final mediaId = selectedList[i];
|
||||
|
|
@ -350,7 +350,7 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
|
||||
try {
|
||||
await _showProgressDialog(
|
||||
'Exporting memories...',
|
||||
context.lang.memoriesExportingProgress,
|
||||
(setProgress) async {
|
||||
for (var i = 0; i < selectedList.length; i++) {
|
||||
final mediaId = selectedList[i];
|
||||
|
|
@ -411,7 +411,9 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
final targetFav = !areAllFav;
|
||||
|
||||
await _showProgressDialog(
|
||||
targetFav ? 'Adding to favorites...' : 'Removing from favorites...',
|
||||
targetFav
|
||||
? context.lang.memoriesAddingToFavorites
|
||||
: context.lang.memoriesRemovingFromFavorites,
|
||||
(setProgress) async {
|
||||
for (var i = 0; i < selectedList.length; i++) {
|
||||
final mediaId = selectedList[i];
|
||||
|
|
|
|||
|
|
@ -207,6 +207,19 @@ class _SynchronizedImageViewerScreenState
|
|||
Future<void> _exportFile() async {
|
||||
final item = widget.galleryItems[_currentIndex].mediaService;
|
||||
|
||||
if (!item.storedPath.existsSync()) {
|
||||
await item.storeMediaFile();
|
||||
if (!mounted) return;
|
||||
if (userService.currentUser.storeMediaFilesInGallery) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.galleryExportSuccess,
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (item.mediaFile.type == MediaType.video) {
|
||||
await saveVideoToGallery(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
final TextEditingController usernameCtrl = TextEditingController();
|
||||
final TextEditingController passwordCtrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
usernameCtrl.dispose();
|
||||
passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _recoverTwonlySafe() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
||||
Log.error('User ID already token. Tying again.');
|
||||
await deleteLocalUserData();
|
||||
return createNewUser();
|
||||
return await createNewUser();
|
||||
}
|
||||
if (res.error == ErrorCode.UsernameAlreadyTaken ||
|
||||
res.error == ErrorCode.UsernameNotValid) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import 'package:twonly/src/services/user.service.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/memories_backup_detail.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart';
|
||||
|
|
@ -74,6 +76,9 @@ class _BackupViewState extends State<BackupView> {
|
|||
Widget build(BuildContext context) {
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
final isFreePlan = currentPlan == SubscriptionPlan.Free;
|
||||
final hasPasswordless =
|
||||
userService.currentUser.passwordLessRecovery != null;
|
||||
final hasPassword = userService.currentUser.isBackupEnabled;
|
||||
|
||||
return StreamBuilder<void>(
|
||||
stream: userService.onUserUpdated,
|
||||
|
|
@ -86,7 +91,33 @@ class _BackupViewState extends State<BackupView> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ListView(
|
||||
children: [
|
||||
if (userService.currentUser.passwordLessRecovery != null)
|
||||
// --- BEREICH 1: Konto-Wiederherstellung ---
|
||||
Text(
|
||||
context.lang.backupRecoverySectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
(!hasPasswordless && !hasPassword)
|
||||
? context.lang.backupRecoverySectionDescNone
|
||||
: context.lang.backupRecoverySectionDescSome,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: (!hasPasswordless && !hasPassword)
|
||||
? context.color.error
|
||||
: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (hasPasswordless)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: PasswordLessRecoveryStatus(),
|
||||
|
|
@ -94,20 +125,46 @@ class _BackupViewState extends State<BackupView> {
|
|||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: () =>
|
||||
context.navPush(const PasswordLessRecoverySetup()),
|
||||
child: Text(context.lang.passwordlessRecoveryEnableBtn),
|
||||
),
|
||||
child: RecoveryCard(
|
||||
icon: FontAwesomeIcons.shieldHeart,
|
||||
title: context.lang.backupRecoveryOptionAFriends,
|
||||
subtitle: context.lang.backupRecoveryOptionAMicrocopy,
|
||||
onTap: () =>
|
||||
context.navPush(const PasswordLessRecoverySetup()),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: RecoveryCard(
|
||||
isEnabled: hasPassword,
|
||||
icon: FontAwesomeIcons.key,
|
||||
title: context.lang.backupRecoveryOptionBPassword,
|
||||
subtitle: hasPassword
|
||||
? context.lang.backupChangePassword
|
||||
: context.lang.backupRecoveryOptionBMicrocopy,
|
||||
onTap: () =>
|
||||
context.push(Routes.settingsBackupSetup, extra: true),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const Divider(),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// --- BEREICH 2: Cloud-Backup ---
|
||||
Text(
|
||||
context.lang.backupCloudSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.lang.backupTwonlySafeDesc,
|
||||
context.lang.backupCloudSectionDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
|
|
@ -115,109 +172,125 @@ class _BackupViewState extends State<BackupView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (userService.currentUser.isBackupEnabled) ...[
|
||||
// 1. Identity tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userCheck,
|
||||
text: context.lang.backupIdentityHeader,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.identityLastSuccessFull,
|
||||
_backupStatus?.identitySize,
|
||||
),
|
||||
// Kontakte & Nachrichten
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.comments,
|
||||
text: context.lang.backupCloudContactsMessages,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.archiveLastSuccessFull,
|
||||
_backupStatus?.archiveSize,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.check_circle, color: Colors.green),
|
||||
),
|
||||
|
||||
// 2. Contacts & Messages tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.comments,
|
||||
text: context.lang.backupArchiveHeader,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.archiveLastSuccessFull,
|
||||
_backupStatus?.archiveSize,
|
||||
),
|
||||
),
|
||||
// Bilder & Medien
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.photoFilm,
|
||||
text: context.lang.backupCloudImagesMedia,
|
||||
subtitle: Text(
|
||||
isFreePlan
|
||||
? context.lang.backupMemoriesUpgradeRequired
|
||||
: (!userService.currentUser.isCloudBackupEnabled
|
||||
? context.lang.backupMemoriesNotEnabled
|
||||
: (_memoriesUsage != null
|
||||
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
||||
: '-')),
|
||||
),
|
||||
trailing: isFreePlan
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade700,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.star,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.lang.backupCloudProBadge,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Switch(
|
||||
value: false,
|
||||
onChanged: null,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Switch(
|
||||
value: userService.currentUser.isCloudBackupEnabled,
|
||||
onChanged: (val) async {
|
||||
if (!val) {
|
||||
final disabled =
|
||||
await promptAndDisableMemoriesBackup(context);
|
||||
if (disabled && mounted) setState(() {});
|
||||
} else {
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = val,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
unawaited(memoriesCloudService.checkUploads());
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: isFreePlan
|
||||
? () async {
|
||||
await context.push(Routes.settingsSubscription);
|
||||
}
|
||||
: () async {
|
||||
if (userService.currentUser.isCloudBackupEnabled) {
|
||||
await context.navPush(
|
||||
const MemoriesBackupDetailView(),
|
||||
);
|
||||
} else {
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = true,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
unawaited(memoriesCloudService.checkUploads());
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// 3. Memories tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.photoFilm,
|
||||
text: context.lang.memoriesBackupTitle,
|
||||
subtitle: Text(
|
||||
isFreePlan
|
||||
? context.lang.backupMemoriesUpgradeRequired
|
||||
: (!userService.currentUser.isCloudBackupEnabled
|
||||
? context.lang.backupMemoriesNotEnabled
|
||||
: (_memoriesUsage != null
|
||||
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
||||
: '-')),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: context.color.onSurfaceVariant,
|
||||
),
|
||||
onTap: () async {
|
||||
if (isFreePlan) {
|
||||
await context.push(Routes.settingsSubscription);
|
||||
} else if (!userService
|
||||
.currentUser
|
||||
.isCloudBackupEnabled) {
|
||||
await UserService.update(
|
||||
(u) => u.isCloudBackupEnabled = true,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
unawaited(memoriesCloudService.checkUploads());
|
||||
} else {
|
||||
await context.navPush(const MemoriesBackupDetailView());
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (userService.currentUser.isBackupEnabled) ...[
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await BackupService.makeBackup(force: true);
|
||||
await _loadBackupStatus();
|
||||
},
|
||||
child: Text(context.lang.backupTwonlySaveNow),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: () => context.push(
|
||||
Routes.settingsBackupSetup,
|
||||
extra: true,
|
||||
),
|
||||
child: Text(
|
||||
!userService.currentUser.isBackupEnabled
|
||||
? context.lang.backupEnableBackup
|
||||
: context.lang.backupChangePassword,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await BackupService.makeBackup(force: true);
|
||||
await _loadBackupStatus();
|
||||
},
|
||||
child: Text(context.lang.backupTwonlySaveNow),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
|||
const SizedBox(width: 8),
|
||||
Text(
|
||||
userService.currentUser.isBackupEnabled
|
||||
? context.lang.backupEnableBackup
|
||||
: context.lang.backupChangePassword,
|
||||
? context.lang.backupChangePassword
|
||||
: context.lang.backupEnableBackup,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
111
lib/src/visual/views/settings/backup/backup_utils.dart
Normal file
111
lib/src/visual/views/settings/backup/backup_utils.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
Future<bool> promptAndDisableMemoriesBackup(BuildContext context) async {
|
||||
final cloudOnlyCount =
|
||||
await twonlyDB.mediaFilesDao.getCloudOnlyMemoriesCount();
|
||||
|
||||
if (!context.mounted) return false;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.settingsStorageDisableBackupTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: formattedText(
|
||||
context,
|
||||
context.lang.settingsStorageDisableBackupBody(
|
||||
cloudOnlyCount,
|
||||
),
|
||||
textColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryMiddle,
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(context.lang.galleryCancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.errorMiddle,
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
context.lang.settingsStorageDisableBackupBtn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
await apiService.disableMemoriesBackup();
|
||||
final allMedias = await (twonlyDB.select(
|
||||
twonlyDB.mediaFiles,
|
||||
)..where((t) => t.stored.equals(true))).get();
|
||||
for (final media in allMedias) {
|
||||
final ms = MediaFileService(media);
|
||||
if (!ms.storedPath.existsSync()) {
|
||||
ms.fullMediaRemoval();
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId);
|
||||
}
|
||||
}
|
||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||
const MediaFilesCompanion(
|
||||
cloudState: Value(CloudState.none),
|
||||
),
|
||||
);
|
||||
await UserService.update((u) => u.isCloudBackupEnabled = false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
showSnackbar(context, e.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -22,7 +22,9 @@ class _MissingBackupCompState extends State<MissingBackupComp> {
|
|||
builder: (context, snapshot) {
|
||||
final user = userService.currentUser;
|
||||
|
||||
if (user.currentSetupPage != null || user.isBackupEnabled) {
|
||||
if (user.currentSetupPage != null ||
|
||||
user.isBackupEnabled ||
|
||||
user.passwordLessRecovery != null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class RecoveryCard extends StatelessWidget {
|
||||
const RecoveryCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
this.isEnabled = false,
|
||||
});
|
||||
final dynamic icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final VoidCallback onTap;
|
||||
final bool isEnabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveColor = isEnabled ? context.color.primary : Colors.red;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: context.color.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: icon is IconData
|
||||
? Icon(
|
||||
icon as IconData,
|
||||
color: effectiveColor,
|
||||
size: 24,
|
||||
)
|
||||
: FaIcon(
|
||||
icon as FaIconData?,
|
||||
color: effectiveColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.color.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: context.color.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||
as server;
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart';
|
||||
|
||||
class MemoriesBackupDetailView extends StatefulWidget {
|
||||
|
|
@ -24,7 +20,6 @@ class MemoriesBackupDetailView extends StatefulWidget {
|
|||
|
||||
class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
||||
server.Response_MemoriesUsage? _memoriesUsage;
|
||||
int _cloudOnlyCount = 0;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
|
|
@ -35,110 +30,18 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
|||
|
||||
Future<void> _loadStats() async {
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
final cloudOnlyCount = await twonlyDB.mediaFilesDao
|
||||
.getCloudOnlyMemoriesCount();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_memoriesUsage = memoriesUsage;
|
||||
_cloudOnlyCount = cloudOnlyCount;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disableBackup() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.settingsStorageDisableBackupTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: formattedText(
|
||||
context,
|
||||
context.lang.settingsStorageDisableBackupBody(
|
||||
_cloudOnlyCount,
|
||||
),
|
||||
textColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryMiddle,
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(context.lang.galleryCancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.errorMiddle,
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
context.lang.settingsStorageDisableBackupBtn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
await apiService.disableMemoriesBackup();
|
||||
final allMedias = await (twonlyDB.select(
|
||||
twonlyDB.mediaFiles,
|
||||
)..where((t) => t.stored.equals(true))).get();
|
||||
for (final media in allMedias) {
|
||||
final ms = MediaFileService(media);
|
||||
if (!ms.storedPath.existsSync()) {
|
||||
ms.fullMediaRemoval();
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId);
|
||||
}
|
||||
}
|
||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||
const MediaFilesCompanion(
|
||||
cloudState: Value(CloudState.none),
|
||||
),
|
||||
);
|
||||
await UserService.update((u) => u.isCloudBackupEnabled = false);
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showSnackbar(context, e.toString());
|
||||
}
|
||||
}
|
||||
final disabled = await promptAndDisableMemoriesBackup(context);
|
||||
if (disabled && mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ class DataAndStorageView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||
late Future<Map<MediaType, int>> _storageStatsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_storageStatsFuture = twonlyDB.mediaFilesDao.getStorageStats();
|
||||
}
|
||||
|
||||
Future<void> showAutoDownloadOptions(
|
||||
BuildContext context,
|
||||
ConnectivityResult connectionMode,
|
||||
|
|
@ -93,7 +101,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
return ListView(
|
||||
children: [
|
||||
FutureBuilder<Map<MediaType, int>>(
|
||||
future: twonlyDB.mediaFilesDao.getStorageStats(),
|
||||
future: _storageStatsFuture,
|
||||
builder: (context, snapshot) {
|
||||
final stats = snapshot.data ?? {};
|
||||
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/cached_network_image.dart';
|
||||
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,9 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> updateUserAvatar(String json, String svg) async {
|
||||
Future<void> updateUserAvatar(String svg) async {
|
||||
await UserService.update(
|
||||
(u) => u
|
||||
..avatarJson = json
|
||||
..avatarSvg = svg
|
||||
..avatarCounter = u.avatarCounter + 1,
|
||||
);
|
||||
|
|
@ -104,9 +103,8 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
|
||||
Future<void> storeAvatarAndExit() async {
|
||||
await _avatarMakerController.saveAvatarSVG();
|
||||
final json = _avatarMakerController.getJsonOptionsSync();
|
||||
final svg = _avatarMakerController.getAvatarSVGSync();
|
||||
await updateUserAvatar(json, svg);
|
||||
await updateUserAvatar(svg);
|
||||
if (mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
|
|
@ -118,8 +116,8 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
if (_avatarMakerController.getJsonOptionsSync() !=
|
||||
userService.currentUser.avatarJson) {
|
||||
if (_avatarMakerController.getAvatarSVGSync() !=
|
||||
userService.currentUser.avatarSvg) {
|
||||
// there where changes
|
||||
final shouldPop = await _showBackDialog() ?? false;
|
||||
if (context.mounted && shouldPop) {
|
||||
|
|
|
|||
234
pubspec.lock
234
pubspec.lock
|
|
@ -18,7 +18,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.3.68"
|
||||
adaptive_number:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/adaptive_number"
|
||||
relative: true
|
||||
|
|
@ -91,19 +91,17 @@ packages:
|
|||
audio_waveforms:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_waveforms
|
||||
sha256: "03b3430ecf430a2e90185518a228c02be3d26653c62dd931e50d671213a6dbc8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "dependencies/audio_waveforms"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.0.2"
|
||||
avatar_maker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: avatar_maker
|
||||
sha256: ca182e33343846427da68fc226325f630063da2de2f0bdb49e683f0c843b80c2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
path: "dependencies/avatar_maker"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.5.0"
|
||||
background_downloader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -115,10 +113,9 @@ packages:
|
|||
blurhash_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: blurhash_dart
|
||||
sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "dependencies/blurhash_dart"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.2.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
|
|
@ -184,30 +181,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.5"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
camera:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -217,14 +190,12 @@ packages:
|
|||
source: hosted
|
||||
version: "0.12.0+1"
|
||||
camera_android_camerax:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/camera/camera_android_camerax"
|
||||
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||
resolved-ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||
url: "https://github.com/otsmr/flutter-packages.git"
|
||||
source: git
|
||||
version: "0.7.1+2"
|
||||
path: "dependencies/camera_android_camerax"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.7.4+6"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -237,10 +208,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: camera_platform_interface
|
||||
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
|
||||
sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.1"
|
||||
camera_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -410,7 +381,7 @@ packages:
|
|||
source: hosted
|
||||
version: "8.1.0"
|
||||
dots_indicator:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/dots_indicator"
|
||||
relative: true
|
||||
|
|
@ -441,7 +412,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.3.0"
|
||||
ed25519_edwards:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/ed25519_edwards"
|
||||
relative: true
|
||||
|
|
@ -628,14 +599,6 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.9.1"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -986,7 +949,7 @@ packages:
|
|||
source: path
|
||||
version: "2.3.0"
|
||||
hashlib_codecs:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/hashlib_codecs"
|
||||
relative: true
|
||||
|
|
@ -1035,11 +998,10 @@ packages:
|
|||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
path: "dependencies/image"
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.9.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1145,10 +1107,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
version: "0.20.3"
|
||||
introduction_screen:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1294,10 +1256,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
version: "0.12.20"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1306,14 +1268,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
material_symbols_icons:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: bd513edc2bc9b034108d5518c48cf5cebbb67d0653728cc452368cb27ca80bb0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2960.0"
|
||||
meta:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1361,16 +1331,8 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
optional:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/optional"
|
||||
relative: true
|
||||
|
|
@ -1552,7 +1514,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/pointycastle"
|
||||
relative: true
|
||||
|
|
@ -1577,11 +1539,10 @@ packages:
|
|||
pro_video_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pro_video_editor
|
||||
sha256: cfed1424b3ca3d5981cc81efdd20b844c995c0ad2818e185eb5bc06a8674f728
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.14.2"
|
||||
path: "dependencies/pro_video_editor"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.11.3"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1623,7 +1584,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.5.0"
|
||||
qr:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/qr"
|
||||
relative: true
|
||||
|
|
@ -1658,14 +1619,6 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
screen_protector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1815,52 +1768,12 @@ packages:
|
|||
source: hosted
|
||||
version: "1.10.2"
|
||||
sprintf:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/sprintf"
|
||||
relative: true
|
||||
source: path
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2+3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1933,14 +1846,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1953,10 +1858,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.12"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2073,10 +1978,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.4.2"
|
||||
very_good_analysis:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -2088,35 +1993,32 @@ packages:
|
|||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.1"
|
||||
path: "dependencies/video_player"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.14.0"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.5"
|
||||
path: "dependencies/video_player_android"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.12.0"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
path: "dependencies/video_player_avfoundation"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.11.0"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
version: "6.9.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2222,7 +2124,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.9.1+1"
|
||||
x25519:
|
||||
dependency: "direct overridden"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "dependencies/x25519"
|
||||
relative: true
|
||||
|
|
|
|||
167
pubspec.yaml
167
pubspec.yaml
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
|||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.5.0+168
|
||||
version: 0.5.1+171
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
|
@ -37,7 +37,6 @@ dependencies:
|
|||
path_provider: ^2.1.5
|
||||
url_launcher: ^6.3.2
|
||||
vector_graphics: ^1.1.19
|
||||
video_player: ^2.10.1
|
||||
in_app_purchase: ^3.3.0
|
||||
go_router: ^17.1.0
|
||||
|
||||
|
|
@ -72,34 +71,13 @@ dependencies:
|
|||
# Overwritten by self-controlled repository
|
||||
emoji_picker_flutter: ^4.3.0
|
||||
|
||||
# Packages which got overwritten using the twonly-app-dependencies repository
|
||||
# Idea: Every change goes though a git commit, where every change can be reviewed.
|
||||
restart_app: ^1.3.2
|
||||
photo_view: ^0.15.0
|
||||
hashlib: ^2.0.0
|
||||
libsignal_protocol_dart: ^0.7.4
|
||||
lottie: ^3.3.1
|
||||
mutex: ^3.1.0
|
||||
introduction_screen: ^4.0.0
|
||||
qr_flutter: ^4.1.0
|
||||
hand_signature: ^3.0.3
|
||||
flutter_sharing_intent: ^2.0.4
|
||||
screen_protector: ^1.5.1
|
||||
flutter_markdown_plus: ^1.0.7
|
||||
exif: ^3.3.0
|
||||
flutter_blurhash: ^0.9.1
|
||||
|
||||
# With high download. (But should be checked nonetheless.)
|
||||
app_links: ^7.0.0 # 1.6 mio
|
||||
image: ^4.3.0 # 3.3 mio
|
||||
flutter_secure_storage: ^10.3.1 # 1.85 mio
|
||||
permission_handler: ^12.0.0+1 # 2 mio
|
||||
|
||||
# Not yet checked
|
||||
audio_waveforms: ^2.0.2
|
||||
avatar_maker: ^0.4.0
|
||||
background_downloader: ^9.4.0
|
||||
cached_network_image: ^3.4.1
|
||||
cryptography_flutter_plus: ^3.0.0
|
||||
cryptography_plus: ^3.0.0
|
||||
flutter_android_volume_keydown: ^1.0.1
|
||||
|
|
@ -109,60 +87,47 @@ dependencies:
|
|||
photo_manager: ^3.9.0
|
||||
google_mlkit_barcode_scanning: ^0.14.1
|
||||
google_mlkit_face_detection: ^0.13.1
|
||||
pro_video_editor: ^1.6.1
|
||||
rust_lib_twonly:
|
||||
path: rust_builder
|
||||
flutter_rust_bridge: 2.12.0
|
||||
|
||||
## --- Start Managed Dependencies ---
|
||||
audio_waveforms: ^2.0.2
|
||||
avatar_maker: ^1.5.0
|
||||
blurhash_dart: ^1.2.1
|
||||
exif: ^3.3.0
|
||||
sprintf: ^7.0.0
|
||||
flutter_blurhash: ^0.9.1
|
||||
flutter_markdown_plus: ^1.0.7
|
||||
video_player: ^2.14.0
|
||||
video_player_android: ^2.12.0
|
||||
video_player_avfoundation: ^2.11.0
|
||||
camera_android_camerax: ^0.7.4+6
|
||||
flutter_sharing_intent: ^2.0.4
|
||||
hand_signature: ^3.1.0+2
|
||||
hashlib: ^2.3.0
|
||||
hashlib_codecs: ^3.0.1
|
||||
image: ^4.9.2
|
||||
introduction_screen: ^4.0.0
|
||||
dots_indicator: ^4.0.1
|
||||
libsignal_protocol_dart: ^0.8.0
|
||||
adaptive_number: ^1.0.0
|
||||
ed25519_edwards: ^0.3.1
|
||||
optional: ^6.1.0+1
|
||||
pointycastle: ^4.0.0
|
||||
x25519: ^0.1.1
|
||||
lottie: ^3.5.1
|
||||
mutex: ^3.1.0
|
||||
photo_view: ^0.15.0
|
||||
pro_video_editor: ^2.11.3
|
||||
qr_flutter: ^4.1.0
|
||||
qr: ^3.1.0-wip
|
||||
restart_app: ^1.7.3
|
||||
screen_protector: ^1.5.1
|
||||
## --- End Managed Dependencies ---
|
||||
|
||||
|
||||
dependency_overrides:
|
||||
dots_indicator:
|
||||
path: ./dependencies/dots_indicator
|
||||
restart_app:
|
||||
path: ./dependencies/restart_app
|
||||
hashlib:
|
||||
path: ./dependencies/hashlib
|
||||
introduction_screen:
|
||||
path: ./dependencies/introduction_screen
|
||||
libsignal_protocol_dart:
|
||||
path: ./dependencies/libsignal_protocol_dart
|
||||
flutter_sharing_intent:
|
||||
path: ./dependencies/flutter_sharing_intent
|
||||
lottie:
|
||||
path: ./dependencies/lottie
|
||||
mutex:
|
||||
path: ./dependencies/mutex
|
||||
photo_view:
|
||||
path: ./dependencies/photo_view
|
||||
qr:
|
||||
path: ./dependencies/qr
|
||||
adaptive_number:
|
||||
path: ./dependencies/adaptive_number
|
||||
ed25519_edwards:
|
||||
path: ./dependencies/ed25519_edwards
|
||||
hand_signature:
|
||||
path: ./dependencies/hand_signature
|
||||
hashlib_codecs:
|
||||
path: ./dependencies/hashlib_codecs
|
||||
optional:
|
||||
path: ./dependencies/optional
|
||||
pointycastle:
|
||||
path: ./dependencies/pointycastle
|
||||
x25519:
|
||||
path: ./dependencies/x25519
|
||||
qr_flutter:
|
||||
path: ./dependencies/qr_flutter
|
||||
screen_protector:
|
||||
path: ./dependencies/screen_protector
|
||||
flutter_markdown_plus:
|
||||
path: ./dependencies/flutter_markdown_plus
|
||||
camera_android_camerax:
|
||||
# path: ../flutter-packages/packages/camera/camera_android_camerax
|
||||
git:
|
||||
url: https://github.com/otsmr/flutter-packages.git
|
||||
path: packages/camera/camera_android_camerax
|
||||
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||
emoji_picker_flutter:
|
||||
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
|
||||
# Using override until this gets merged.
|
||||
|
|
@ -173,12 +138,74 @@ dependency_overrides:
|
|||
git:
|
||||
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
|
||||
ref: fix/lStar-not-found-error
|
||||
|
||||
## --- Start Managed Dependency Overrides ---
|
||||
audio_waveforms:
|
||||
path: ./dependencies/audio_waveforms
|
||||
avatar_maker:
|
||||
path: ./dependencies/avatar_maker
|
||||
blurhash_dart:
|
||||
path: ./dependencies/blurhash_dart
|
||||
exif:
|
||||
path: ./dependencies/exif
|
||||
sprintf:
|
||||
path: ./dependencies/sprintf
|
||||
flutter_blurhash:
|
||||
path: ./dependencies/flutter_blurhash
|
||||
flutter_markdown_plus:
|
||||
path: ./dependencies/flutter_markdown_plus
|
||||
video_player:
|
||||
path: ./dependencies/video_player
|
||||
video_player_android:
|
||||
path: ./dependencies/video_player_android
|
||||
video_player_avfoundation:
|
||||
path: ./dependencies/video_player_avfoundation
|
||||
camera_android_camerax:
|
||||
path: ./dependencies/camera_android_camerax
|
||||
flutter_sharing_intent:
|
||||
path: ./dependencies/flutter_sharing_intent
|
||||
hand_signature:
|
||||
path: ./dependencies/hand_signature
|
||||
hashlib:
|
||||
path: ./dependencies/hashlib
|
||||
hashlib_codecs:
|
||||
path: ./dependencies/hashlib_codecs
|
||||
image:
|
||||
path: ./dependencies/image
|
||||
introduction_screen:
|
||||
path: ./dependencies/introduction_screen
|
||||
dots_indicator:
|
||||
path: ./dependencies/dots_indicator
|
||||
libsignal_protocol_dart:
|
||||
path: ./dependencies/libsignal_protocol_dart
|
||||
adaptive_number:
|
||||
path: ./dependencies/adaptive_number
|
||||
ed25519_edwards:
|
||||
path: ./dependencies/ed25519_edwards
|
||||
optional:
|
||||
path: ./dependencies/optional
|
||||
pointycastle:
|
||||
path: ./dependencies/pointycastle
|
||||
x25519:
|
||||
path: ./dependencies/x25519
|
||||
lottie:
|
||||
path: ./dependencies/lottie
|
||||
mutex:
|
||||
path: ./dependencies/mutex
|
||||
photo_view:
|
||||
path: ./dependencies/photo_view
|
||||
pro_video_editor:
|
||||
path: ./dependencies/pro_video_editor
|
||||
qr_flutter:
|
||||
path: ./dependencies/qr_flutter
|
||||
qr:
|
||||
path: ./dependencies/qr
|
||||
restart_app:
|
||||
path: ./dependencies/restart_app
|
||||
screen_protector:
|
||||
path: ./dependencies/screen_protector
|
||||
## --- End Managed Dependency Overrides ---
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.15
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pro_video_editor/core/platform/io/io_helper.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parse_link.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ class UserEnvironment {
|
|||
appVersion: 100,
|
||||
);
|
||||
|
||||
// ignore: cascade_invocations
|
||||
us.isUserCreated = true;
|
||||
|
||||
final identityKeyPair = generateIdentityKeyPair();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('BetterText parses URLs correctly', (WidgetTester tester) async {
|
||||
testWidgets('BetterText parses URLs correctly', (tester) async {
|
||||
const text =
|
||||
'Test: (https://google.com) and another link https://example.com/#fragment, plus www.test.com. Also check https://wikipedia.org/wiki/Test_(disambiguation) !';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue