mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +00:00
fix #303 and bump version
This commit is contained in:
parent
fa85dbd6e2
commit
8a4632c4c4
14 changed files with 493 additions and 11 deletions
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
## 0.0.69
|
## 0.0.69
|
||||||
|
|
||||||
|
- Option to export and import memories
|
||||||
|
- iOS support for ultra-wide-angle camera
|
||||||
- Support Android Monochrome Icon
|
- Support Android Monochrome Icon
|
||||||
|
- Multiple layout issues fixed
|
||||||
- Multiple bug fixes
|
- Multiple bug fixes
|
||||||
|
|
||||||
## 0.0.67
|
## 0.0.67
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,37 @@ PODS:
|
||||||
- Flutter
|
- Flutter
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- DKImagePickerController/Core (4.3.9):
|
||||||
|
- DKImagePickerController/ImageDataManager
|
||||||
|
- DKImagePickerController/Resource
|
||||||
|
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||||
|
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||||
|
- DKImagePickerController/Core
|
||||||
|
- DKPhotoGallery
|
||||||
|
- DKImagePickerController/Resource (4.3.9)
|
||||||
|
- DKPhotoGallery (0.0.19):
|
||||||
|
- DKPhotoGallery/Core (= 0.0.19)
|
||||||
|
- DKPhotoGallery/Model (= 0.0.19)
|
||||||
|
- DKPhotoGallery/Preview (= 0.0.19)
|
||||||
|
- DKPhotoGallery/Resource (= 0.0.19)
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Core (0.0.19):
|
||||||
|
- DKPhotoGallery/Model
|
||||||
|
- DKPhotoGallery/Preview
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Model (0.0.19):
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Preview (0.0.19):
|
||||||
|
- DKPhotoGallery/Model
|
||||||
|
- DKPhotoGallery/Resource
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Resource (0.0.19):
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
- emoji_picker_flutter (0.0.1):
|
- emoji_picker_flutter (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ffmpeg_kit_flutter_new (1.0.0):
|
- ffmpeg_kit_flutter_new (1.0.0):
|
||||||
|
|
@ -18,6 +49,9 @@ PODS:
|
||||||
- Flutter
|
- Flutter
|
||||||
- ffmpeg_kit_flutter_new/full-gpl (1.0.0):
|
- ffmpeg_kit_flutter_new/full-gpl (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- file_picker (0.0.1):
|
||||||
|
- DKImagePickerController/PhotoGallery
|
||||||
|
- Flutter
|
||||||
- Firebase (12.4.0):
|
- Firebase (12.4.0):
|
||||||
- Firebase/Core (= 12.4.0)
|
- Firebase/Core (= 12.4.0)
|
||||||
- Firebase/Core (12.4.0):
|
- Firebase/Core (12.4.0):
|
||||||
|
|
@ -249,6 +283,7 @@ PODS:
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
- sqlite3/session
|
- sqlite3/session
|
||||||
- SwiftProtobuf (1.33.1)
|
- SwiftProtobuf (1.33.1)
|
||||||
|
- SwiftyGif (5.4.5)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- video_player_avfoundation (0.0.1):
|
- video_player_avfoundation (0.0.1):
|
||||||
|
|
@ -264,6 +299,7 @@ DEPENDENCIES:
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
|
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
|
||||||
- ffmpeg_kit_flutter_new (from `.symlinks/plugins/ffmpeg_kit_flutter_new/ios`)
|
- ffmpeg_kit_flutter_new (from `.symlinks/plugins/ffmpeg_kit_flutter_new/ios`)
|
||||||
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Firebase
|
- Firebase
|
||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||||
|
|
@ -298,6 +334,8 @@ DEPENDENCIES:
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
|
- DKImagePickerController
|
||||||
|
- DKPhotoGallery
|
||||||
- Firebase
|
- Firebase
|
||||||
- FirebaseAnalytics
|
- FirebaseAnalytics
|
||||||
- FirebaseCore
|
- FirebaseCore
|
||||||
|
|
@ -318,6 +356,7 @@ SPEC REPOS:
|
||||||
- Sentry
|
- Sentry
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftProtobuf
|
- SwiftProtobuf
|
||||||
|
- SwiftyGif
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
audio_waveforms:
|
audio_waveforms:
|
||||||
|
|
@ -336,6 +375,8 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
|
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
|
||||||
ffmpeg_kit_flutter_new:
|
ffmpeg_kit_flutter_new:
|
||||||
:path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios"
|
:path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios"
|
||||||
|
file_picker:
|
||||||
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
:path: ".symlinks/plugins/firebase_core/ios"
|
:path: ".symlinks/plugins/firebase_core/ios"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
|
|
@ -394,8 +435,11 @@ SPEC CHECKSUMS:
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
|
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
|
||||||
ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f
|
ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f
|
||||||
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||||
|
|
@ -439,6 +483,7 @@ SPEC CHECKSUMS:
|
||||||
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
||||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
||||||
SwiftProtobuf: 533a18409c3ca3a6156b2b1e46afd0f69e751aba
|
SwiftProtobuf: 533a18409c3ca3a6156b2b1e46afd0f69e751aba
|
||||||
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -827,5 +827,7 @@
|
||||||
"avatarSaveChangesStore": "Speichern",
|
"avatarSaveChangesStore": "Speichern",
|
||||||
"avatarSaveChangesDiscard": "Verwerfen",
|
"avatarSaveChangesDiscard": "Verwerfen",
|
||||||
"inProcess": "Wird verarbeitet",
|
"inProcess": "Wird verarbeitet",
|
||||||
"draftMessage": "Entwurf"
|
"draftMessage": "Entwurf",
|
||||||
|
"exportMemories": "Memories exportieren (Beta)",
|
||||||
|
"importMemories": "Memories importieren (Beta)"
|
||||||
}
|
}
|
||||||
|
|
@ -605,5 +605,7 @@
|
||||||
"avatarSaveChangesStore": "Save",
|
"avatarSaveChangesStore": "Save",
|
||||||
"avatarSaveChangesDiscard": "Discard",
|
"avatarSaveChangesDiscard": "Discard",
|
||||||
"inProcess": "In process",
|
"inProcess": "In process",
|
||||||
"draftMessage": "Draft"
|
"draftMessage": "Draft",
|
||||||
|
"exportMemories": "Export memories (Beta)",
|
||||||
|
"importMemories": "Import memories (Beta)"
|
||||||
}
|
}
|
||||||
|
|
@ -2737,6 +2737,18 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Draft'**
|
/// **'Draft'**
|
||||||
String get draftMessage;
|
String get draftMessage;
|
||||||
|
|
||||||
|
/// No description provided for @exportMemories.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Export memories (Beta)'**
|
||||||
|
String get exportMemories;
|
||||||
|
|
||||||
|
/// No description provided for @importMemories.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import memories (Beta)'**
|
||||||
|
String get importMemories;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1510,4 +1510,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get draftMessage => 'Entwurf';
|
String get draftMessage => 'Entwurf';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportMemories => 'Memories exportieren (Beta)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importMemories => 'Memories importieren (Beta)';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1500,4 +1500,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get draftMessage => 'Draft';
|
String get draftMessage => 'Draft';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportMemories => 'Export memories (Beta)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importMemories => 'Import memories (Beta)';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class MediaFileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> purgeTempFolder() async {
|
static Future<void> purgeTempFolder() async {
|
||||||
final tempDirectory = MediaFileService._buildDirectoryPath(
|
final tempDirectory = MediaFileService.buildDirectoryPath(
|
||||||
'tmp',
|
'tmp',
|
||||||
await getApplicationSupportDirectory(),
|
await getApplicationSupportDirectory(),
|
||||||
);
|
);
|
||||||
|
|
@ -239,7 +239,7 @@ class MediaFileService {
|
||||||
await updateFromDB();
|
await updateFromDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Directory _buildDirectoryPath(
|
static Directory buildDirectoryPath(
|
||||||
String directory,
|
String directory,
|
||||||
Directory applicationSupportDirectory,
|
Directory applicationSupportDirectory,
|
||||||
) {
|
) {
|
||||||
|
|
@ -275,7 +275,7 @@ class MediaFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final mediaBaseDir =
|
final mediaBaseDir =
|
||||||
_buildDirectoryPath(directory, applicationSupportDirectory);
|
buildDirectoryPath(directory, applicationSupportDirectory);
|
||||||
return File(
|
return File(
|
||||||
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,7 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
_imageSaving = true;
|
_imageSaving = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (widget.mediaService.mediaFile.type == MediaType.image ||
|
|
||||||
widget.mediaService.mediaFile.type == MediaType.gif) {
|
|
||||||
await widget.storeImageAsOriginal();
|
await widget.storeImageAsOriginal();
|
||||||
}
|
|
||||||
|
|
||||||
String? res;
|
String? res;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
import 'package:twonly/src/views/settings/data_and_storage/export_media.view.dart';
|
||||||
|
import 'package:twonly/src/views/settings/data_and_storage/import_media.view.dart';
|
||||||
|
|
||||||
class DataAndStorageView extends StatefulWidget {
|
class DataAndStorageView extends StatefulWidget {
|
||||||
const DataAndStorageView({super.key});
|
const DataAndStorageView({super.key});
|
||||||
|
|
@ -62,6 +64,36 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
onChanged: (a) => toggleStoreInGallery(),
|
onChanged: (a) => toggleStoreInGallery(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
context.lang.exportMemories,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) {
|
||||||
|
return const ExportMediaView();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
context.lang.importMemories,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) {
|
||||||
|
return const ImportMediaView();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
|
||||||
175
lib/src/views/settings/data_and_storage/export_media.view.dart
Normal file
175
lib/src/views/settings/data_and_storage/export_media.view.dart
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
|
||||||
|
class ExportMediaView extends StatefulWidget {
|
||||||
|
const ExportMediaView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExportMediaView> createState() => _ExportMediaViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExportMediaViewState extends State<ExportMediaView> {
|
||||||
|
double _progress = 0;
|
||||||
|
String? _status;
|
||||||
|
File? _zipFile;
|
||||||
|
bool _isZipping = false;
|
||||||
|
bool _zipSaved = false;
|
||||||
|
|
||||||
|
Future<Directory> _mediaFolder() async {
|
||||||
|
final dir = MediaFileService.buildDirectoryPath(
|
||||||
|
'stored',
|
||||||
|
await getApplicationSupportDirectory(),
|
||||||
|
);
|
||||||
|
if (!dir.existsSync()) await dir.create(recursive: true);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createZipFromMediaFolder() async {
|
||||||
|
setState(() {
|
||||||
|
_isZipping = true;
|
||||||
|
_progress = 0.0;
|
||||||
|
_status = 'Preparing...';
|
||||||
|
_zipFile = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final folder = await _mediaFolder();
|
||||||
|
final allFiles =
|
||||||
|
folder.listSync(recursive: true).whereType<File>().toList();
|
||||||
|
|
||||||
|
final mediaFiles = allFiles.where((f) {
|
||||||
|
final name = p.basename(f.path).toLowerCase();
|
||||||
|
if (name.contains('thumbnail')) return false;
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (mediaFiles.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'No memories found.';
|
||||||
|
_isZipping = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute total bytes for progress
|
||||||
|
var totalBytes = 0;
|
||||||
|
for (final f in mediaFiles) {
|
||||||
|
totalBytes += await f.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final zipPath = p.join(
|
||||||
|
tempDir.path,
|
||||||
|
'memories.zip',
|
||||||
|
);
|
||||||
|
final encoder = ZipFileEncoder()..create(zipPath);
|
||||||
|
|
||||||
|
var processedBytes = 0;
|
||||||
|
for (final f in mediaFiles) {
|
||||||
|
final relative = p.relative(f.path, from: folder.path);
|
||||||
|
setState(() {
|
||||||
|
_status = 'Adding $relative';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ZipFileEncoder doesn't give per-file progress; update after adding.
|
||||||
|
await encoder.addFile(f, relative);
|
||||||
|
|
||||||
|
processedBytes += await f.length();
|
||||||
|
setState(() {
|
||||||
|
_progress = totalBytes > 0 ? processedBytes / totalBytes : 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Future.delayed(
|
||||||
|
const Duration(milliseconds: 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await encoder.close();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_zipFile = File(zipPath);
|
||||||
|
_status = 'ZIP created: ${p.basename(zipPath)}';
|
||||||
|
_progress = 1.0;
|
||||||
|
_isZipping = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'Error: $e';
|
||||||
|
_isZipping = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveZip() async {
|
||||||
|
if (_zipFile == null) return;
|
||||||
|
try {
|
||||||
|
final outputFile = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: 'Save your memories to desired location',
|
||||||
|
fileName: p.basename(_zipFile!.path),
|
||||||
|
bytes: _zipFile!.readAsBytesSync(),
|
||||||
|
);
|
||||||
|
if (outputFile == null) return;
|
||||||
|
_zipSaved = true;
|
||||||
|
_status = 'ZIP stored: ${p.basename(_zipFile!.path)}';
|
||||||
|
setState(() {});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _status = 'Save failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Export memories'),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
margin: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Here, you can export all you memories.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_isZipping || _zipFile != null)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: _isZipping ? _progress : (_zipFile != null ? 1.0 : 0.0),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_status != null)
|
||||||
|
Text(
|
||||||
|
_status!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_zipFile == null)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.archive),
|
||||||
|
label: Text(
|
||||||
|
_isZipping ? 'Zipping...' : 'Create ZIP from mediafiles',
|
||||||
|
),
|
||||||
|
onPressed: _isZipping ? null : _createZipFromMediaFolder,
|
||||||
|
)
|
||||||
|
else if (!_zipSaved)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.save_alt),
|
||||||
|
label: const Text('Save ZIP'),
|
||||||
|
onPressed: (_zipFile != null && !_isZipping) ? _saveZip : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
lib/src/views/settings/data_and_storage/import_media.view.dart
Normal file
192
lib/src/views/settings/data_and_storage/import_media.view.dart
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:twonly/globals.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';
|
||||||
|
|
||||||
|
class ImportMediaView extends StatefulWidget {
|
||||||
|
const ImportMediaView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportMediaView> createState() => _ImportMediaViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportMediaViewState extends State<ImportMediaView> {
|
||||||
|
double _progress = 0;
|
||||||
|
String? _status;
|
||||||
|
File? _zipFile;
|
||||||
|
bool _isProcessing = false;
|
||||||
|
|
||||||
|
Future<void> _pickAndImportZip() async {
|
||||||
|
setState(() {
|
||||||
|
_status = null;
|
||||||
|
_progress = 0;
|
||||||
|
_zipFile = null;
|
||||||
|
_isProcessing = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['zip'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null || result.files.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'No file selected.';
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pickedPath = result.files.single.path;
|
||||||
|
if (pickedPath == null) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'Selected file has no path.';
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pickedFile = File(pickedPath);
|
||||||
|
if (!pickedFile.existsSync()) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'Selected file does not exist.';
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_zipFile = pickedFile;
|
||||||
|
_status = 'Selected ${p.basename(pickedPath)}';
|
||||||
|
});
|
||||||
|
|
||||||
|
await _extractZipToMediaFolder(pickedFile);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'Error: $e';
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _extractZipToMediaFolder(File zipFile) async {
|
||||||
|
setState(() {
|
||||||
|
_status = 'Reading archive...';
|
||||||
|
_progress = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read zip bytes and decode
|
||||||
|
final bytes = await zipFile.readAsBytes();
|
||||||
|
final archive = ZipDecoder().decodeBytes(bytes);
|
||||||
|
|
||||||
|
// Optionally: compute total entries to show progress
|
||||||
|
final entries = archive.where((e) => e.isFile).toList();
|
||||||
|
final total = entries.length;
|
||||||
|
var processed = 0;
|
||||||
|
|
||||||
|
for (final file in entries) {
|
||||||
|
if (!file.isFile || file.isSymbolicLink) continue;
|
||||||
|
|
||||||
|
final extSplit = file.name.split('.');
|
||||||
|
if (extSplit.isEmpty) continue;
|
||||||
|
final ext = extSplit.last;
|
||||||
|
|
||||||
|
late MediaType type;
|
||||||
|
switch (ext) {
|
||||||
|
case 'webp':
|
||||||
|
type = MediaType.image;
|
||||||
|
case 'mp4':
|
||||||
|
type = MediaType.video;
|
||||||
|
case 'gif':
|
||||||
|
type = MediaType.gif;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
|
MediaFilesCompanion(
|
||||||
|
type: Value(type),
|
||||||
|
createdAt: Value(file.lastModDateTime),
|
||||||
|
stored: const Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final mediaService = await MediaFileService.fromMedia(mediaFile!);
|
||||||
|
await mediaService.storedPath.writeAsBytes(file.content);
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
setState(() {
|
||||||
|
_progress = total > 0 ? processed / total : 0;
|
||||||
|
_status = 'Imported ${file.name}';
|
||||||
|
});
|
||||||
|
|
||||||
|
// allow UI to update for large archives
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_status = 'Import complete. ${entries.length} entries processed.';
|
||||||
|
_isProcessing = false;
|
||||||
|
_progress = 1;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_status = 'Extraction failed: $e';
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Import memories'),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
margin: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Here, you can import exported memories.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_isProcessing || _zipFile != null)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value:
|
||||||
|
_isProcessing ? _progress : (_zipFile != null ? 1.0 : 0.0),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_status != null)
|
||||||
|
Text(
|
||||||
|
_status!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.file_upload),
|
||||||
|
label: Text(
|
||||||
|
_isProcessing
|
||||||
|
? 'Processing...'
|
||||||
|
: 'Select memories.zip to import',
|
||||||
|
),
|
||||||
|
onPressed: _isProcessing ? null : _pickAndImportZip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
pubspec.lock
10
pubspec.lock
|
|
@ -34,7 +34,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.4.1"
|
version: "8.4.1"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
|
@ -442,6 +442,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.3.6"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.0.68+68
|
version: 0.0.69+69
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.6.0
|
sdk: ^3.6.0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
archive: ^4.0.7
|
||||||
audio_waveforms: ^1.3.0
|
audio_waveforms: ^1.3.0
|
||||||
avatar_maker: ^0.4.0
|
avatar_maker: ^0.4.0
|
||||||
background_downloader: ^9.2.2
|
background_downloader: ^9.2.2
|
||||||
|
|
@ -23,6 +24,7 @@ dependencies:
|
||||||
drift_flutter: ^0.2.4
|
drift_flutter: ^0.2.4
|
||||||
emoji_picker_flutter: ^4.3.0
|
emoji_picker_flutter: ^4.3.0
|
||||||
ffmpeg_kit_flutter_new: ^4.1.0
|
ffmpeg_kit_flutter_new: ^4.1.0
|
||||||
|
file_picker: ^10.3.6
|
||||||
firebase_core: ^4.2.0
|
firebase_core: ^4.2.0
|
||||||
firebase_messaging: ^16.0.3
|
firebase_messaging: ^16.0.3
|
||||||
fixnum: ^1.1.1
|
fixnum: ^1.1.1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue