{"id":35166043,"url":"https://github.com/functionland/fxfiles","last_synced_at":"2026-05-27T05:05:36.951Z","repository":{"id":328662095,"uuid":"1114904136","full_name":"functionland/FxFiles","owner":"functionland","description":"FxFiles Flutter app integrated with S3 API","archived":false,"fork":false,"pushed_at":"2026-02-26T04:06:15.000Z","size":14032,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-26T09:50:27.000Z","etag":null,"topics":["blockchain","dapps-development","decentralized-applications","flutter-apps","ipfs","ipfs-api"],"latest_commit_sha":null,"homepage":"https://play.google.com/store/apps/details?id=land.fx.files","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/functionland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-12T03:59:17.000Z","updated_at":"2026-02-26T04:06:20.000Z","dependencies_parsed_at":null,"dependency_job_id":"69f7bc1f-f9c1-427e-8887-28937038e2a2","html_url":"https://github.com/functionland/FxFiles","commit_stats":null,"previous_names":["functionland/fxfiles"],"tags_count":32,"template":false,"template_full_name":null,"purl":"pkg:github/functionland/FxFiles","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2FFxFiles","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2FFxFiles/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2FFxFiles/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2FFxFiles/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/functionland","download_url":"https://codeload.github.com/functionland/FxFiles/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2FFxFiles/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30113106,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-05T03:40:26.266Z","status":"ssl_error","status_checked_at":"2026-03-05T03:39:15.902Z","response_time":93,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blockchain","dapps-development","decentralized-applications","flutter-apps","ipfs","ipfs-api"],"created_at":"2025-12-28T19:33:10.597Z","updated_at":"2026-05-12T01:08:19.037Z","avatar_url":"https://github.com/functionland.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FxFiles - Fula File Manager\n\nA minimalistic file manager with Fula decentralized storage backup support. Built with Flutter for cross-platform compatibility.\n\n## Features\n\n- **Local File Browser**: Browse and manage local files and folders\n- **Fula Cloud Storage**: Sync files to decentralized Fula network\n- **Client-Side Encryption**: AES-256-GCM encryption before upload\n- **Authentication**: Sign in with Google or Apple\n- **File Viewers**: Built-in viewers for images, videos, and text files\n- **Text Extraction (OCR)**: Extract text from any image on-device — tap, copy, done. All processing stays on your phone, fully private (iOS \u0026 Android)\n- **Search**: Search local files by name\n- **Trash Management**: Safely delete and restore files\n- **Dark/Light Theme**: Automatic theme switching\n- **NFT Generation**: Generate NFTs from your photos and share them with friends and family to claim with zero gas fees — no smart contracts, no fees, just import your asset and generate\n- **Zero-Gas NFT Claiming**: Recipients claim NFTs with absolutely no gas fees via a server-side relay — perfect for non-crypto people who don't even have a wallet. No wallet top-ups, no confusing transaction popups. Just sign, claim, and it's yours\n- **OpenSea Integration**: Minted NFTs show up on OpenSea automatically — ready to view, share, or show off\n- **NFT Transfer**: Standard ERC1155 transfers between wallets (FULA stays locked)\n- **NFT Burn-to-Release**: Burn NFTs to permanently destroy them and release locked FULA to the burner\n- **Dual Wallet Support**: Internal wallet (auto-derived from sign-in) or external wallet (WalletConnect)\n\n## Architecture\n\n```\nlib/\n├── app/\n│   ├── app.dart              # Root widget\n│   ├── router.dart           # GoRouter navigation\n│   └── theme/                # Theme configuration\n├── core/\n│   ├── models/               # Data models (LocalFile, FulaObject, SyncState, NftToken)\n│   └── services/             # Core services\n│       ├── auth_service.dart         # Google/Apple authentication\n│       ├── encryption_service.dart   # AES-256-GCM encryption\n│       ├── file_service.dart         # Local file operations\n│       ├── fula_api_service.dart     # Fula S3-compatible API\n│       ├── local_storage_service.dart # Hive local storage\n│       ├── nft_service.dart          # NFT mint/claim/burn/transfer orchestration\n│       ├── nft_contract_service.dart  # ABI encoding + RPC calls for NFT contract\n│       ├── nft_wallet_service.dart    # Deterministic wallet derivation + signing\n│       ├── text_recognition_service.dart # On-device OCR text extraction (ML Kit)\n│       ├── secure_storage_service.dart # Secure key storage\n│       └── sync_service.dart         # File synchronization\n├── features/\n│   ├── browser/              # Local file browser\n│   ├── fula/                 # Fula cloud browser\n│   ├── home/                 # Home screen with categories\n│   ├── nft/                  # NFT minting \u0026 claiming\n│   │   ├── providers/        # NftNotifier, nftTagsProvider, nftMintsProvider\n│   │   ├── screens/          # Collection browser, detail, claim\n│   │   └── widgets/          # NftCard, MintConfigDialog, ClaimShareSheet\n│   ├── search/               # File search\n│   ├── settings/             # App settings\n│   ├── shared/               # Shared files\n│   ├── sync/                 # Sync providers\n│   ├── trash/                # Trash management\n│   └── viewer/               # File viewers (image, video, text)\n├── shared/\n│   └── widgets/              # Reusable widgets\n└── main.dart                 # App entry point\n```\n\n## Getting Started\n\n### Prerequisites\n\n- Flutter SDK 3.2.0 or higher\n- Dart SDK 3.2.0 or higher\n- Android Studio / Xcode for mobile development\n\n### Installation\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/user/FxFiles.git\ncd FxFiles\n```\n\n2. Install dependencies:\n```bash\nflutter pub get\n```\n\n3. Run the app:\n```bash\nflutter run\n```\n\n### Windows Build\n\n#### Prerequisites\n\n- Flutter SDK 3.38.0+\n- Rust (install from [rustup.rs](https://rustup.rs))\n- `flutter_rust_bridge_codegen`: `cargo install flutter_rust_bridge_codegen`\n\n#### Build and Run\n\n```powershell\n# 1. Get dependencies\nflutter pub get\n\n# 2. Generate Dart/Rust bindings (from repo root of fula-api)\nflutter_rust_bridge_codegen generate\n\n# 3. Build the native DLL (from repo root of fula-api)\ncargo build -p fula-flutter --release\n\n# 4. Copy DLL to the FxFiles windows directory\ncopy path\\to\\fula-api\\target\\release\\fula_flutter.dll windows\\fula_flutter.dll\n\n# 5. Run the app\nflutter run -d windows\n```\n\nThe CMakeLists.txt automatically copies `fula_flutter.dll` from `windows/` to the build output directory.\n\n#### Build MSIX Installer\n\n```powershell\n# 1. Ensure fula_flutter.dll is in windows/ (see steps above)\n\n# 2. Add msix dev dependency (if not already added)\ndart pub add --dev msix\n\n# 3. Build Windows release\nflutter build windows --release\n\n# 4. Create MSIX installer\ndart run msix:create\n```\n\nThe MSIX installer will be at `build/windows/x64/runner/Release/fula_files.msix`. Double-click to install.\n\n### Configuration\n\n#### Fula API Setup\n\n1. Open the app and go to **Settings**\n2. Under **Fula Configuration**, enter:\n   - **API Gateway URL**: Your Fula gateway endpoint (e.g., `https://gateway.fula.network`)\n   - **JWT Token**: Your authentication token\n   - **IPFS Server** (optional): Custom IPFS server URL\n\n#### Authentication\n\nSign in with Google or Apple to enable:\n- Encrypted file uploads\n- Per-user encryption keys derived from authentication\n- Cross-device sync\n\n## Security\n\n### Encryption\n\n- **Algorithm**: AES-256-GCM for symmetric encryption\n- **Key Derivation**: PBKDF2 with SHA-256 for password-based keys\n- **Per-User Keys**: Encryption keys derived from user authentication\n- **Client-Side**: All encryption happens locally before upload\n\n### Data Flow\n\n```\nLocal File → Encrypt (AES-256-GCM) → Upload to Fula → IPFS Storage\n                    ↑\n            User's Encryption Key\n```\n\n## Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `flutter_riverpod` | State management |\n| `go_router` | Navigation |\n| `minio_new` | S3-compatible API client |\n| `cryptography` | Encryption primitives |\n| `hive_flutter` | Local storage |\n| `flutter_secure_storage` | Secure key storage |\n| `google_sign_in` | Google authentication |\n| `sign_in_with_apple` | Apple authentication |\n| `workmanager` | Background sync tasks |\n| `connectivity_plus` | Network monitoring |\n| `google_mlkit_text_recognition` | On-device OCR text extraction from images |\n| `reown_appkit` | WalletConnect wallet connection (external wallet for NFTs) |\n| `web3dart` | EVM wallet derivation + transaction signing (internal wallet) |\n\n## Usage\n\n### Upload Files to Fula\n\n1. Browse to local files\n2. Select files to upload\n3. Tap the upload button\n4. Files are encrypted and uploaded automatically\n\n### Download from Fula\n\n1. Open **Fula Browser** from home screen\n2. Browse buckets and files\n3. Tap a file to download and decrypt\n\n### Sync Folders\n\n```dart\nawait SyncService.instance.syncFolder(\n  localPath: '/storage/emulated/0/Documents',\n  remoteBucket: 'my-bucket',\n  remotePrefix: 'documents',\n  direction: SyncDirection.bidirectional,\n);\n```\n\n## API Reference\n\n### SyncService\n\n```dart\n// Queue file upload\nawait SyncService.instance.queueUpload(\n  localPath: '/path/to/file.txt',\n  remoteBucket: 'my-bucket',\n  remoteKey: 'file.txt',\n  encrypt: true,\n);\n\n// Queue file download\nawait SyncService.instance.queueDownload(\n  remoteBucket: 'my-bucket',\n  remoteKey: 'file.txt',\n  localPath: '/path/to/file.txt',\n  decrypt: true,\n);\n\n// Retry failed syncs\nawait SyncService.instance.retryFailed();\n```\n\n### FulaApiService\n\n```dart\n// List objects in bucket\nfinal objects = await FulaApiService.instance.listObjects(\n  'my-bucket',\n  prefix: 'folder/',\n);\n\n// Upload with encryption\nawait FulaApiService.instance.encryptAndUpload(\n  'my-bucket',\n  'file.txt',\n  fileBytes,\n  encryptionKey,\n);\n\n// Download and decrypt\nfinal bytes = await FulaApiService.instance.downloadAndDecrypt(\n  'my-bucket',\n  'file.txt',\n  encryptionKey,\n);\n```\n\n### Multipart Upload (Large Files)\n\nFor files larger than 5MB, multipart upload is used automatically with progress tracking:\n\n```dart\n// Upload large file with progress (\u003e5MB uses multipart automatically)\nawait FulaApiService.instance.uploadLargeFile(\n  'my-bucket',\n  'large-video.mp4',\n  fileBytes,\n  onProgress: (progress) {\n    print('Upload: ${progress.percentage.toStringAsFixed(1)}%');\n  },\n);\n\n// Encrypted large file upload\nawait FulaApiService.instance.encryptAndUploadLargeFile(\n  'my-bucket',\n  'large-video.mp4',\n  fileBytes,\n  encryptionKey,\n  originalFilename: 'vacation.mp4',\n  onProgress: (progress) {\n    print('Encrypted upload: ${progress.percentage.toStringAsFixed(1)}%');\n  },\n);\n```\n\n### Background Sync (WorkManager)\n\nBackground sync runs automatically when the app is closed:\n\n```dart\n// Initialize background sync (called in main.dart)\nawait BackgroundSyncService.instance.initialize();\n\n// Schedule periodic sync (every 15 minutes on WiFi)\nawait BackgroundSyncService.instance.schedulePeriodicSync(\n  frequency: Duration(minutes: 15),\n  requiresWifi: true,\n);\n\n// Schedule one-time upload\nawait BackgroundSyncService.instance.scheduleUpload(\n  localPath: '/path/to/file.txt',\n  bucket: 'my-bucket',\n  key: 'file.txt',\n  encrypt: true,\n  useMultipart: true,\n);\n\n// Schedule one-time download\nawait BackgroundSyncService.instance.scheduleDownload(\n  bucket: 'my-bucket',\n  key: 'file.txt',\n  localPath: '/path/to/file.txt',\n  decrypt: true,\n);\n\n// Retry failed operations\nawait BackgroundSyncService.instance.scheduleRetryFailed();\n\n// Cancel all background tasks\nawait BackgroundSyncService.instance.cancelAll();\n```\n\n### Secure Sharing (HPKE)\n\nShare encrypted files with others without exposing your master key. Three sharing modes are available:\n\n#### Share Types\n\n| Type | Use Case | Security |\n|------|----------|----------|\n| **Create Link For...** | Share with specific recipient | Highest - uses recipient's public key |\n| **Create Link** | Anyone with link can access | Medium - disposable keypair in URL fragment |\n| **Create Link with Password** | Password-protected access | High - password + link required |\n\n#### Gateway URL Structure\n\nAll share links use the gateway at `https://cloud.fx.land/view`:\n\n```\nhttps://cloud.fx.land/view/{shareId}#{payload}\n```\n\n**URL Structure:**\n- `{shareId}` - Unique share identifier (UUID)\n- `#{payload}` - Base64url-encoded payload in URL fragment (never sent to server)\n\n**Payload Contents (for public/password links):**\n```json\n{\n  \"v\": 1,              // Version\n  \"t\": \"\u003ctoken\u003e\",      // Encoded share token\n  \"k\": \"\u003csecretKey\u003e\",  // Link secret key (base64)\n  \"b\": \"bucket-name\",  // Storage bucket\n  \"p\": \"/path/to/file\", // Path scope\n  \"pwd\": false         // Is password-protected\n}\n```\n\n#### API Usage\n\n```dart\n// Get your public key to share with others\nfinal myPublicKey = await AuthService.instance.getPublicKeyString();\n\n// 1. Create a share for a specific recipient\nfinal token = await SharingService.instance.shareWithUser(\n  pathScope: '/photos/vacation/',\n  bucket: 'my-bucket',\n  recipientPublicKey: recipientPublicKeyBytes,\n  recipientName: 'John',\n  dek: folderEncryptionKey,\n  permissions: SharePermissions.readOnly,\n  expiryDays: 30,\n  label: 'Vacation photos',\n);\n\n// 2. Create a public link (anyone with link can access)\nfinal publicLink = await SharingService.instance.createPublicLink(\n  pathScope: '/photos/vacation/',\n  bucket: 'my-bucket',\n  dek: folderEncryptionKey,\n  expiryDays: 7,\n  label: 'Vacation photos',\n);\n// Result: https://cloud.fx.land/view/abc123#eyJ2IjoxLC...\n\n// 3. Create a password-protected link\nfinal passwordLink = await SharingService.instance.createPasswordProtectedLink(\n  pathScope: '/documents/report.pdf',\n  bucket: 'my-bucket',\n  dek: folderEncryptionKey,\n  expiryDays: 30,\n  password: 'secretPassword123',\n  label: 'Monthly report',\n);\n// Recipients need both the link AND password to access\n\n// Generate share link from OutgoingShare\nfinal link = SharingService.instance.generateShareLinkFromOutgoing(outgoingShare);\n\n// Accept a share from link\nfinal accepted = await SharingService.instance.acceptShareFromString(encodedToken);\nprint('Access to: ${accepted.pathScope}');\nprint('Can write: ${accepted.canWrite}');\n\n// Revoke a share\nawait SharingService.instance.revokeShare(shareId);\n```\n\n#### Expiry Options\n\n| Option | Duration |\n|--------|----------|\n| 1 Day | 24 hours |\n| 1 Week | 7 days |\n| 1 Month | 30 days |\n| 1 Year | 365 days |\n| 5 Years | 1825 days (max) |\n\n#### Share Modes\n\n- **Temporal**: Recipients see the latest version of the file/folder\n- **Snapshot**: Recipients only see the specific version at share time\n\n**Sharing Model:**\n- **Path-Scoped**: Share only specific folders or files\n- **Time-Limited**: Access expires automatically\n- **Permission-Based**: Read-only, read-write, or full access\n- **Revocable**: Cancel access at any time\n- **Zero Knowledge**: Server can't read shared content\n\n**Security Details:**\n- URL fragment (`#...`) is never sent to server (HTTP standard)\n- Public links use disposable X25519 keypairs - private key in fragment\n- Password links encrypt the payload with PBKDF2-SHA256 derived key\n- All keys stored locally, synced to cloud encrypted with user's master key\n- Each share is isolated - revoking one doesn't affect others\n\n**How it works:**\n1. Owner creates share token with appropriate encryption\n2. For recipient shares: DEK re-encrypted using HPKE (X25519 + AES-256-GCM)\n3. For public links: Disposable keypair generated, private key in URL fragment\n4. For password links: Payload encrypted with password-derived key\n5. Token sent via any channel (link, QR code, message)\n6. Recipient decrypts with their private key or password\n7. Owner's master key is never exposed\n\n#### Cloud Storage Structure for Shares (Owner's Share List)\n\nThe gateway can retrieve an owner's share list from cloud storage. Shares are stored encrypted with the owner's encryption key:\n\n**Storage Location:**\n```\nBucket: fula-metadata\nKey:    .fula/shares/{userId}.json.enc\n```\n\n**User ID Derivation:**\n```dart\n// userId is first 16 chars of SHA-256(publicKey), URL-safe\nfinal hash = sha256(publicKeyBytes);\nfinal userId = hash.substring(0, 16).replaceAll('/', '_').replaceAll('+', '-');\n```\n\n**Decrypted JSON Structure:**\n```json\n{\n  \"version\": 1,\n  \"updatedAt\": \"2024-01-15T10:30:00.000Z\",\n  \"shares\": [\n    {\n      \"id\": \"share-uuid\",\n      \"token\": {\n        \"id\": \"token-uuid\",\n        \"pathScope\": \"/photos/vacation/\",\n        \"permissions\": \"readOnly\",\n        \"wrappedDek\": \"base64...\",\n        \"recipientPublicKey\": \"base64...\",\n        \"issuedAt\": \"2024-01-15T10:30:00.000Z\",\n        \"expiresAt\": \"2024-02-15T10:30:00.000Z\",\n        \"shareType\": \"publicLink\",\n        \"shareMode\": \"temporal\"\n      },\n      \"bucket\": \"photos\",\n      \"recipientName\": \"Public Link\",\n      \"label\": \"Vacation photos\",\n      \"sharedAt\": \"2024-01-15T10:30:00.000Z\",\n      \"isRevoked\": false,\n      \"linkSecretKey\": \"base64...\",\n      \"passwordSalt\": null\n    }\n  ]\n}\n```\n\n**OutgoingShare Fields:**\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | string | Unique share identifier |\n| `token` | ShareToken | The share token with encryption details |\n| `bucket` | string | S3 bucket containing shared content |\n| `recipientName` | string | Display name (or \"Public Link\"/\"Password Link\") |\n| `label` | string? | Optional user-defined label |\n| `sharedAt` | datetime | When share was created |\n| `isRevoked` | bool | Whether share has been revoked |\n| `linkSecretKey` | string? | Base64 secret key for public links |\n| `passwordSalt` | string? | Base64 salt for password-protected links |\n\n### Audio Playlists Cloud Storage\n\nPlaylists are stored encrypted for recovery across devices:\n\n**Storage Location:**\n```\nBucket: playlists\nKey:    user-playlists/{playlistId}.json\n```\n\n**Decrypted JSON Structure:**\n```json\n{\n  \"id\": \"playlist-uuid\",\n  \"name\": \"My Favorites\",\n  \"tracks\": [\n    {\n      \"id\": \"track-uuid\",\n      \"path\": \"/storage/emulated/0/Music/song.mp3\",\n      \"name\": \"Song Title\",\n      \"artist\": \"Artist Name\",\n      \"album\": \"Album Name\",\n      \"duration\": 180000,\n      \"artworkPath\": \"/path/to/artwork.jpg\"\n    }\n  ],\n  \"createdAt\": \"2024-01-15T10:30:00.000Z\",\n  \"updatedAt\": \"2024-01-20T15:45:00.000Z\",\n  \"cloudKey\": \"user-playlists/playlist-uuid.json\",\n  \"isSyncedToCloud\": true\n}\n```\n\n**Playlist Fields:**\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | string | Unique playlist identifier |\n| `name` | string | Playlist name |\n| `tracks` | AudioTrack[] | List of tracks in order |\n| `createdAt` | datetime | When playlist was created |\n| `updatedAt` | datetime | Last modification time |\n| `cloudKey` | string? | S3 key if synced to cloud |\n| `isSyncedToCloud` | bool | Whether synced to cloud |\n\n**AudioTrack Fields:**\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | string | Unique track identifier |\n| `path` | string | Local file path |\n| `name` | string | Track/file name |\n| `artist` | string? | Artist name from metadata |\n| `album` | string? | Album name from metadata |\n| `duration` | int | Duration in milliseconds |\n| `artworkPath` | string? | Path to album artwork |\n\n### Gateway Implementation Notes\n\nFor the gateway at `https://cloud.fx.land/view`:\n\n1. **Parsing Share Links:**\n   ```\n   URL: https://cloud.fx.land/view/{shareId}#{payload}\n\n   1. Extract shareId from path\n   2. Extract payload from URL fragment (client-side only)\n   3. Base64url-decode payload\n   4. If password-protected: prompt for password, derive key with PBKDF2\n   5. Decrypt payload to get token, bucket, path, and secretKey\n   6. Use secretKey to decrypt wrappedDek in token\n   7. Use decrypted DEK to access files in bucket/path\n   ```\n\n2. **Fetching Owner's Share List:**\n   ```\n   1. Authenticate owner (get their encryption key)\n   2. Compute userId from owner's public key\n   3. Fetch: fula-metadata/.fula/shares/{userId}.json.enc\n   4. Decrypt with owner's encryption key\n   5. Parse JSON to get list of OutgoingShare objects\n   ```\n\n3. **Fetching User's Playlists:**\n   ```\n   1. Authenticate user (get their encryption key)\n   2. List objects: playlists/user-playlists/*.json\n   3. For each object: download and decrypt with user's key\n   4. Parse JSON to get Playlist objects\n   ```\n\n### NFT Lifecycle\n\nGenerate NFTs from your photos and let your friends and family claim them with zero gas. No smart contracts, no fees — just import your asset, generate an NFT, and share it with your friends or put it on OpenSea. Recipients claim with no wallet top-ups and no confusing transaction popups. Just sign, claim, and it's theirs. NFTs show up on OpenSea automatically — ready to view, share, or show off. Perfect for non-crypto people who don't even have a wallet.\n\nBuilt on ERC1155 tokens with FULA token backing. FULA tokens are permanently locked inside NFTs and can only be released by burning the NFT. Claims are gasless via a server-side meta-transaction relay that sponsors gas on behalf of the claimer.\n\n#### Overview\n\n```\nCreate Collection → Import Images → Generate NFTs → Share Link → Friends Claim (Zero Gas) → View on OpenSea\n```\n\nUsers can:\n- **Generate** NFTs from photos with FULA locked per token — no smart contract knowledge needed\n- **Share** claim links — open (anyone can claim) or targeted (specific wallet)\n- **Claim with zero gas** — server-side relay pays gas fees so recipients never need tokens or wallet top-ups\n- **Transfer** NFTs freely via standard ERC1155 transfers (FULA stays locked)\n- **Burn** NFTs to permanently destroy them and release the locked FULA to the burner\n- **View on OpenSea** — minted NFTs appear on OpenSea automatically\n\n#### Wallet Options\n\nEvery NFT operation shows a **wallet picker** before proceeding:\n\n| Wallet | Source | Use Case |\n|--------|--------|----------|\n| **Internal Wallet** | Auto-derived from sign-in credentials via HMAC-SHA256 + secp256k1 (web3dart) | Non-crypto users — no setup required |\n| **Connected Wallet** | External wallet via Reown AppKit (WalletConnect) | Crypto users who want to use MetaMask, etc. |\n\nIf only one wallet is available, it auto-selects. If neither is available, the user is prompted to sign in or connect.\n\n**Internal wallet derivation:**\n```\nencryptionKey = Argon2id(\"{provider}:{userId}:{email}\", \"fula-files-v1\") → 32 bytes (existing)\nnftPrivateKey = HMAC-SHA256(\"nft-wallet\", encryptionKey) → 32 bytes\nnftAddress    = EthPrivateKey(nftPrivateKey).address → \"0x...\"\n```\n\nThe internal wallet uses web3dart's `EthPrivateKey` for proper secp256k1 address derivation and `Web3Client.sendTransaction()` for on-chain signing.\n\n#### 1. Generating NFTs\n\n**User flow:** Create Collection → Import Photos → Choose Wallet → Configure (count, FULA/NFT, chain) → Generate\n\n**Pipeline (5 checkpointed steps):**\n\n```\nUpload to IPFS → Approve FULA spend → mintWithFula() → Poll Receipt → Parse Token ID\n```\n\n1. **Upload to IPFS** — Image uploaded unencrypted to Fula S3 gateway, CID returned\n2. **Approve FULA** — ERC20 `approve()` so the NFT contract can pull FULA tokens\n3. **mintWithFula()** — Contract locks FULA and mints ERC1155 tokens\n4. **Poll Receipt** — Wait for transaction confirmation\n5. **Parse Token ID** — Extract minted token ID from receipt logs\n\nEach step is persisted to Hive. If the app crashes or the transaction fails, \"Retry\" resumes from the last successful step (skips upload if CID exists, skips approval if tx exists).\n\nFor internal wallets, approval uses `NftWalletService.sendApproveTransaction()` (ABI-encodes ERC20 approve and signs locally). For external wallets, it goes through `WalletService.sendContractTransaction()` via WalletConnect.\n\n#### 2. Sharing (Claim Links)\n\n**Creator side:** On a minted NFT card → \"Share Claim\" → Configure → Share link\n\nThe claim dialog offers:\n- **\"Anyone can claim\" toggle** (default ON) — creates an open claim where `claimer = address(0)` on-chain. First person to open the link claims it.\n- **Targeted claim** (toggle OFF) — enter a specific wallet address. Only that wallet can claim.\n- **Expiry** — 1, 7, 30, or 90 days.\n\nThe contract escrows 1 NFT via `createClaimOffer()` and returns a `linkHash`. A deep link is generated:\n\n```\nfxfiles://nft-claim?chain={chainId}\u0026contract={address}\u0026token={tokenId}\u0026hash={linkHash}\n```\n\n**Claimer side:** Open link → App shows NFT details (image, creator, FULA/NFT, supply) → Choose Wallet → Claim (zero gas)\n\nThe claim screen fetches token info from the contract via `eth_call`, displays the NFT image from the IPFS gateway. Claims are submitted to the server-side relay (`/api/v1/nft/relay`) which broadcasts the transaction and pays gas on behalf of the claimer. The claimer only signs an EIP-712 message — no gas tokens needed. Pre-claim checks detect already-claimed or expired links.\n\n**Sponsored gas (meta-transaction relay):**\n- Claimer signs an EIP-712 typed message (no on-chain transaction from their wallet)\n- Server relay broadcasts `claimNFTMeta()` using a funded relay wallet\n- On SKALE Europa: gas is inherently free (sFUEL)\n- On Base: relay wallet covers ETH gas costs\n- Result: recipients never need to hold any tokens to claim\n\nAfter claiming, the screen shows **Transfer** and **Burn** buttons.\n\n#### 3. Transfer\n\nStandard ERC1155 `safeTransferFrom()`. FULA tokens remain locked inside the NFT — transfers do NOT release FULA. The UI shows: \"Transfer keeps FULA locked. Only burning releases FULA.\"\n\n**Flow:** Choose Wallet → Enter recipient `0x...` address → Confirm → `safeTransferFrom()` → Poll Receipt\n\nNFTs can be transferred any number of times between any wallets. The FULA only comes out when someone burns.\n\n#### 4. Burn (Releases FULA)\n\nThe **only way** to release locked FULA from an NFT is to burn it. Burning permanently destroys the NFT and sends the proportional FULA to the burner.\n\n**Flow:** Choose Wallet → Confirmation dialog (\"This will permanently destroy the NFT and release X FULA to your wallet. This action cannot be undone.\") → `burn(account, tokenId, amount)` → Poll Receipt\n\nThe contract's `burn()` override:\n1. Calls `super.burn()` — standard ERC1155 burn (checks caller is owner or approved, zeros balance)\n2. Calculates `fulaToRelease = tokenInfo[id].fulaPerNft * amount`\n3. Decrements `totalLockedFula`\n4. Transfers FULA to the burner via `storageToken.safeTransfer()`\n5. Emits `NftBurned(tokenId, burner, amount, fulaReleased)`\n\n`burnBatch()` works similarly for multiple token types in one transaction.\n\n#### Smart Contract: `FulaFileNFT.sol`\n\nERC1155 upgradeable contract using OpenZeppelin v5.3.0 + GovernanceModule (UUPS proxy).\n\nDeployed identically on Base (chain 8453) and Skale Europa (chain 2046399126).\n\n```solidity\n// Mint — caller must approve() FULA spend first\nmintWithFula(string ipfsCid, uint256 fulaPerNft, uint256 count) → uint256 firstTokenId\n\n// Claim — sender creates offer (escrows NFT), recipient claims\ncreateClaimOffer(uint256 tokenId, address claimer, uint256 expiresAt) → bytes32 linkHash\nclaimNFT(bytes32 linkHash)              // direct claim (claimer pays gas)\nclaimNFTMeta(bytes32 secret, address signer, uint256 deadline, uint256 nonce, bytes signature)  // sponsored claim (relay pays gas)\n// claimer = address(0) → open claim (anyone can claim)\n// claimer = 0x1234...  → only that address can claim\n\n// Burn — permanently destroys NFT, releases locked FULA to burner\nburn(address account, uint256 id, uint256 value)       // single token type\nburnBatch(address account, uint256[] ids, uint256[] values)  // multiple types\nburnMeta(bytes32 claimKey, uint256 tokenId, uint256 amount, address signer, uint256 deadline, uint256 nonce, bytes signature)  // sponsored burn\n\n// Transfer — standard ERC1155, FULA stays locked\nsafeTransferFrom(from, to, id, amount, data)  // inherited from ERC1155\n\n// Cancel — sender (after expiry) or admin can cancel stuck offers\ncancelClaimOffer(bytes32 linkHash)\n\n// Read\ngetTokenInfo(uint256 tokenId) → (creator, ipfsCid, fulaAmount, totalSupply)\ngetClaimOffer(bytes32 linkHash) → (tokenId, sender, claimer, expiresAt, claimed)\ngetCreatorTokens(address) → uint256[]\n```\n\n**Security features:**\n- `totalLockedFula` tracking prevents admin `recoverERC20` from draining locked funds\n- Monotonic nonce prevents duplicate claim offer hashes\n- `ipfsCid` length validation (1-256 bytes)\n- `nonReentrant` + `whenNotPaused` on all write functions\n- Burn checks caller is owner or approved (via ERC1155Burnable)\n\n#### Flutter Architecture\n\n| Component | Role |\n|-----------|------|\n| `NftService` | Orchestrates mint/claim/burn/transfer flows, Hive persistence, cloud sync |\n| `NftContractService` | ABI encoding, RPC calls, receipt polling |\n| `NftWalletService` | Deterministic wallet derivation (web3dart), transaction signing |\n| `WalletService` | External wallet via Reown AppKit (WalletConnect) |\n| `NftNotifier` (Riverpod) | UI state: `isMinting`, `isClaiming`, `isBurning`, `isTransferring`, `error` |\n| `nftMintsProvider` | `StreamProvider.family` — real-time mint status updates per collection |\n| `nftTagsProvider` | Filters tags with `nft-` prefix to list NFT collections |\n\n**Wallet dispatch:** All transaction methods accept a `WalletSource` enum (`internal` or `external`). Internal uses `NftWalletService.sendSignedTransaction()` (web3dart `Web3Client`). External uses `WalletService.sendContractTransaction()` (Reown AppKit).\n\n#### Data Models (Hive)\n\n| Type ID | Model | Description |\n|---------|-------|-------------|\n| 30 | `NftCollection` | Collection with tagId, name, list of mints |\n| 31 | `NftMintRecord` | Mint state: CID, txHash, tokenId, status, claims |\n| 32 | `NftClaimRecord` | Claim state: linkHash, claimer, status, tx hashes |\n| 33 | `NftMintStatus` | Enum: approving, minting, confirming, completed, error |\n| 34 | `NftClaimStatus` | Enum: pending, claimed, burned, expired |\n\n#### Cloud Sync\n\nNFT metadata is encrypted and synced to the `nft-metadata` S3 bucket:\n\n```\nBucket: nft-metadata\nKey:    .fula/nfts/{userId}.json\n```\n\nOn login, `restoreFromCloud()` merges cloud data with local state, preserving any in-progress mints. Sync is debounced (5s) and triggered after mints, claims, and burns.\n\n#### Deep Link Format\n\n```\nfxfiles://nft-claim?chain=8453\u0026contract=0x...\u0026token=123\u0026hash=0xabc...\n```\n\n#### After Contract Deployment\n\nReplace the zero-address placeholders in `SupportedChain.nftContractAddress` with the real proxy addresses for each chain.\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch\n3. Commit changes\n4. Push to the branch\n5. Open a Pull Request\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n\n## Acknowledgments\n\n- [Fula Network](https://fula.network) for decentralized storage\n- Flutter team for the amazing framework\n- All open-source contributors\n\n## Demo\n\n[Watch the demo video](https://youtu.be/rhEi1yA14LM)\n\n## TODO\n\n- [ X ] Add repeat track for audio playback. An icon to allow user to click and it puts the playing track on repeat and the icon becomes filled and another click toggles off (Audio)\n- [ X ] Add audio playlist creation and management + Upload the playlist to cloud encrypted nad securely using hte S3 APIs so that user can recover them (Audio)\n- [ X ] Add shuffle functionality for playlists (Audio)\n- [ X ] Add playlist management (rename, delete, reorder) (Audio)\n- [ X ] Add audio visualization in the audio player to beautify it with animated waveforms (Audio)\n- [ X ] Add audio equalizer to adjust bass, treble, and mid frequencies with a simpole click (Audio)\n- [ X ] Add playback control from lock screen (Audio)\n- [ X ] Add playback control from notification tray (Audio)\n- [ X ] Audio track playback continues when going out of audios in the app. the player will become minimized at the bottom of hte screen where user can still interat with buttons and see the progress bar of audio while browsing other files (Audio)\n- [ X ] Add video playback picture-in-picture so user can minimize a playing video and continue browsing other files. also it has the picture in picture feature of app that the minimized video can be seen in android screen whele using other apps too (Videos)\n- [ X ] Add video thumbnail in browsing but it should be optimized and not consume much processing power (Videos)\n- [ X ] Add zip file viewer and unzip functionality to specified location. so in the Archive category in the menu of each zip file, we also ee an unzip item (Archives)\n- [ X ] Add file compression so in all screens like Images, Videos, Documents, when we select a file or multiple files on the top currenly we see a delete and a share icon, we also need to add a compression icon to compress files to zip (Archives, General)\n- [ X ] Bug: Loading large text files hangs up the app, we should add streaming loading and lazy loading for better performance for large text files (Documents)\n- [ X ] Add search text in text viewer so that user can type a text in document viewer search and can go to next or previouse occurances of that text (Documents)\n- [ X ] Add goto line functionality in text viewer that user clicks in document viewer and then enters the line number to jump to and it takes user to that line (Documents)\n- [ X ] Bug: In opened text file in the text viewer, the wrap text does not work (Documents)\n- [ X ] Clicking on a type like pdf that cannot be opned in-app, should open the Android or iOS app selector to open it with the correct app. in other plavces if there is an unknown file type showing in the list that cannot be handled by app, it should open the Android or iOS app selector to open it with the correct app (Documents, General)\n- [ X ] Add Image Editor to be able to crop, rotate, and adjust brightness, contrast, and saturation and write text over image (Images)\n- [ X ] Add swipe right and left gestures in image viewer to go to next and previouse image in Images when an image is opened, but ensure that in zoom mode swipe bedcome deactivated and enhance gesture handlers to differentiate swipe and pinch and pan for zoom clearly (Images)\n- [ X ] In zoom mode of image viewer, double tapping the screen takes it to normal view (images)\n- [ X ] Swiping up the image in image viewer, reveals and shows the file details below the image along with faces that are detected in the image (Images)\n- [ X ] Bug: Reword JWT Token to API Key\n- [ X ] Design: Separate the Starred, Cloud, Shared and Playlists categories under a differnet section, named \"Featured\" below hte Categories section and before \"Storage\" section (General)\n- [ X ] Bug: Starred files are not showing up in the Starred category. Although it seems hte file is starred but hte starred category remains empty (General)\n- [ X ] Bug: In audio player, the first time you open an audio the visualizer stays in loading (Audio)\n- [ X ] Bug: Version in about screen not updating according to latest app version\n- [ X ] Add thumbscroll functionality for better navigation. the header tags shown in thumbscroll mode should be according to the sorting. for exmaple in sorting alphanumerically, the headrs become the letters of filenames like A,B,C,... when in date sort mode the tags become the month-year like Jan-2024, Feb-2024, etc. We should consider all optimizations possible to make it fast and smooth in large folders and in categories and to ensure that the headers are not repeated and updated according to all files in the folder or category without reducing hte performance or making the app laggy. Also it should be a separate module that can be deactivated if user wants (General)\n- [ ] Add folder names in tabs inside each category. default view is All, but user can switch to other tabs in each category like \"Images\" to see only images in that folder for example \"WhatsApp\"\n- [ X ] Add sharing with links where root path is https://cloud.fx.land/ and the rest of parameters are based on the current s3 API doc for encrypted files where we have everything to decrypt a file in the link and it shows the link to user (General) - Implemented three share types: public links, password-protected links, and recipient-specific shares\n- [ X ] Change package name to land.fx.files.dev and create github actions to remove.dev for publishing to play store\n- [ X ] NFT minting: Mint images as ERC1155 NFTs on Base/Skale Europa with FULA token locking (NFT)\n- [ X ] NFT claim links: Share NFTs via deep links — open claims (anyone) or targeted (specific wallet) (NFT)\n- [ X ] NFT burn-to-release: Burn NFTs to permanently destroy and release locked FULA (NFT)\n- [ X ] NFT transfer: Standard ERC1155 transfers, FULA stays locked (NFT)\n- [ X ] NFT internal wallet: Deterministic wallet derivation + web3dart signing for non-crypto users (NFT)\n- [ X ] NFT wallet picker: Choose internal or connected wallet before every operation (NFT)\n- [ X ] NFT cloud sync: Encrypted metadata sync to S3 for cross-device recovery (NFT)\n- [ X ] NFT retry logic: Resume failed mints from last checkpoint (NFT)\n- [ X ] Deploy FulaFileNFT contract and update SupportedChain addresses (NFT)\n- [ X ] Zero-gas NFT claiming: Server-side meta-transaction relay sponsors gas for claimers (NFT)\n- [ X ] Text extraction (OCR): Extract text from images on-device with one tap — copy all or select portions, fully private (Images)\n- [ X ] Implement proper error handling for background sync\n- [ X ] Add unit tests for all services\n- [ X ] Add AI features that interact with blox\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunctionland%2Ffxfiles","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffunctionland%2Ffxfiles","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunctionland%2Ffxfiles/lists"}