diff --git a/.gitignore b/.gitignore
index be82281..5faa5b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,4 +46,12 @@ app.*.map.json
/android/app/release
# Environment Variables
-*.env
\ No newline at end of file
+*.env
+
+# Coverage Report
+coverage/lcov.info
+
+# Secret Files
+odin-release-key.keystore
+android/app/google-services.json
+android/app/build/*
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 56cb953..dd44d58 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -23,10 +23,11 @@ if (flutterVersionName == null) {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
+apply plugin: 'com.google.gms.google-services'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 30
+ compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -43,8 +44,8 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId "com.example.odin"
- minSdkVersion 16
+ applicationId "com.odin.odin"
+ minSdkVersion 19
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -65,4 +66,6 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation platform('com.google.firebase:firebase-bom:29.0.0')
+ implementation 'com.google.firebase:firebase-analytics-ktx'
}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index a242ca4..c91a0b3 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -1,7 +1,9 @@
+ package="com.odin.odin">
-
-
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e44b319..6d044f4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,43 +1,64 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/example/odin/MainActivity.kt b/android/app/src/main/kotlin/com/odin/odin/MainActivity.kt
similarity index 79%
rename from android/app/src/main/kotlin/com/example/odin/MainActivity.kt
rename to android/app/src/main/kotlin/com/odin/odin/MainActivity.kt
index e113e89..29e5583 100644
--- a/android/app/src/main/kotlin/com/example/odin/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/odin/odin/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.example.odin
+package com.odin.odin
import io.flutter.embedding.android.FlutterActivity
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
index a242ca4..ec57716 100644
--- a/android/app/src/profile/AndroidManifest.xml
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -1,5 +1,5 @@
+ package="com.odin.odin">
diff --git a/android/build.gradle b/android/build.gradle
index ed45c65..2ec5821 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,5 +1,5 @@
buildscript {
- ext.kotlin_version = '1.3.50'
+ ext.kotlin_version = '1.4.32'
repositories {
google()
mavenCentral()
@@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.google.gms:google-services:4.3.10'
}
}
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
new file mode 100644
index 0000000..766c39d
--- /dev/null
+++ b/ios/Podfile.lock
@@ -0,0 +1,97 @@
+PODS:
+ - DKImagePickerController/Core (4.3.2):
+ - DKImagePickerController/ImageDataManager
+ - DKImagePickerController/Resource
+ - DKImagePickerController/ImageDataManager (4.3.2)
+ - DKImagePickerController/PhotoGallery (4.3.2):
+ - DKImagePickerController/Core
+ - DKPhotoGallery
+ - DKImagePickerController/Resource (4.3.2)
+ - DKPhotoGallery (0.0.17):
+ - DKPhotoGallery/Core (= 0.0.17)
+ - DKPhotoGallery/Model (= 0.0.17)
+ - DKPhotoGallery/Preview (= 0.0.17)
+ - DKPhotoGallery/Resource (= 0.0.17)
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Core (0.0.17):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Preview
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Model (0.0.17):
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Preview (0.0.17):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Resource
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Resource (0.0.17):
+ - SDWebImage
+ - SwiftyGif
+ - file_picker (0.0.1):
+ - DKImagePickerController/PhotoGallery
+ - Flutter
+ - Flutter (1.0.0)
+ - fluttertoast (0.0.2):
+ - Flutter
+ - Toast
+ - path_provider_ios (0.0.1):
+ - Flutter
+ - SDWebImage (5.12.1):
+ - SDWebImage/Core (= 5.12.1)
+ - SDWebImage/Core (5.12.1)
+ - shared_preferences (0.0.1):
+ - Flutter
+ - SwiftyGif (5.4.0)
+ - Toast (4.0.0)
+ - url_launcher (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - file_picker (from `.symlinks/plugins/file_picker/ios`)
+ - Flutter (from `Flutter`)
+ - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
+ - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
+ - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
+ - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
+
+SPEC REPOS:
+ trunk:
+ - DKImagePickerController
+ - DKPhotoGallery
+ - SDWebImage
+ - SwiftyGif
+ - Toast
+
+EXTERNAL SOURCES:
+ file_picker:
+ :path: ".symlinks/plugins/file_picker/ios"
+ Flutter:
+ :path: Flutter
+ fluttertoast:
+ :path: ".symlinks/plugins/fluttertoast/ios"
+ path_provider_ios:
+ :path: ".symlinks/plugins/path_provider_ios/ios"
+ shared_preferences:
+ :path: ".symlinks/plugins/shared_preferences/ios"
+ url_launcher:
+ :path: ".symlinks/plugins/url_launcher/ios"
+
+SPEC CHECKSUMS:
+ DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
+ DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
+ file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
+ Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
+ fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
+ path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
+ SDWebImage: 4dc3e42d9ec0c1028b960a33ac6b637bb432207b
+ shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273
+ SwiftyGif: 5d4af95df24caf1c570dbbcb32a3b8a0763bc6d7
+ Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
+ url_launcher: b6e016d912f04be9f5bf6e8e82dc599b7ba59649
+
+PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+
+COCOAPODS: 1.10.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index d839b23..55b5035 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 79DF600E59FE316592D4D2EA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B1C0BA13701FEF4705D0592 /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -29,12 +30,16 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 0421B36D6A4BB73AE139E805 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 05FAA6C9911FF316DC32C1AF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 4B1C0BA13701FEF4705D0592 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 86D76059EEF7402A65462524 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -49,12 +54,32 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 79DF600E59FE316592D4D2EA /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 4A06798E3698482BCB9FA510 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 05FAA6C9911FF316DC32C1AF /* Pods-Runner.debug.xcconfig */,
+ 0421B36D6A4BB73AE139E805 /* Pods-Runner.release.xcconfig */,
+ 86D76059EEF7402A65462524 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ 6CEF878642B00B899E8F72A7 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 4B1C0BA13701FEF4705D0592 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -72,6 +97,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
+ 4A06798E3698482BCB9FA510 /* Pods */,
+ 6CEF878642B00B899E8F72A7 /* Frameworks */,
);
sourceTree = "";
};
@@ -105,12 +132,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
+ 54CBB8B844CDA4D94EC33CED /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ DA012354BFB7D40C2D413CB2 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -183,6 +212,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
+ 54CBB8B844CDA4D94EC33CED /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -197,6 +248,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
+ DA012354BFB7D40C2D413CB2 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -291,7 +359,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.example.odin;
+ PRODUCT_BUNDLE_IDENTIFIER = com.odin.odin;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -415,7 +483,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.example.odin;
+ PRODUCT_BUNDLE_IDENTIFIER = com.odin.odin;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -434,7 +502,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.example.odin;
+ PRODUCT_BUNDLE_IDENTIFIER = com.odin.odin;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -468,4 +536,4 @@
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
\ No newline at end of file
+}
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
index 1d526a1..21a3cc1 100644
--- a/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,7 @@
+
+
diff --git a/lib/main.dart b/lib/main.dart
index 4247899..d02bc03 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
+import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -10,7 +11,10 @@ import 'package:odin/pages/home_page.dart';
import 'package:odin/providers/file_notifier.dart';
import 'package:odin/services/locator.dart';
import 'package:odin/services/logger.dart';
+import 'package:odin/services/toast_service.dart';
+import 'package:open_file/open_file.dart';
import 'package:provider/provider.dart';
+import 'package:url_launcher/url_launcher.dart';
void main() async {
if (Platform.environment.containsKey('FLUTTER_TEST')) {
@@ -45,9 +49,68 @@ void main() async {
}
}
-class MyApp extends StatelessWidget {
+class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
+ @override
+ State createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ void initDynamicLinks() async {
+ final _toast = locator();
+ final _fileNotifier = context.read();
+ FirebaseDynamicLinks.instance.onLink(
+ onSuccess: (PendingDynamicLinkData? dynamicLink) async {
+ final Uri? deepLink = dynamicLink?.link;
+ if (deepLink != null) {
+ if (deepLink.pathSegments.isNotEmpty) {
+ if (deepLink.pathSegments[0] == "files") {
+ logger.i(deepLink.pathSegments.last);
+ final String token = deepLink.pathSegments.last;
+ final _filePath =
+ await _fileNotifier.getFileFromToken(token.trim());
+ if (Platform.isWindows || Platform.isMacOS) {
+ launch(_filePath);
+ } else {
+ _toast.showMobileToast("Files saved in Downloads.");
+ await OpenFile.open(_filePath);
+ }
+ }
+ }
+ }
+ }, onError: (OnLinkErrorException e) async {
+ logger.e(e.message, e);
+ });
+ final PendingDynamicLinkData? data =
+ await FirebaseDynamicLinks.instance.getInitialLink();
+ final Uri? deepLink = data?.link;
+
+ if (deepLink != null) {
+ if (deepLink.pathSegments.isNotEmpty) {
+ if (deepLink.pathSegments[0] == "files") {
+ logger.i(deepLink.pathSegments.last);
+ final String token = deepLink.pathSegments.last;
+ final _filePath = await _fileNotifier.getFileFromToken(token.trim());
+ if (Platform.isWindows || Platform.isMacOS) {
+ launch(_filePath);
+ } else {
+ _toast.showMobileToast("Files saved in Downloads.");
+ await OpenFile.open(_filePath);
+ }
+ }
+ }
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ if (Platform.isAndroid || Platform.isIOS) {
+ initDynamicLinks();
+ }
+ }
+
@override
Widget build(BuildContext context) {
return MaterialApp(
diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart
index be59a65..a5d3958 100644
--- a/lib/pages/home_page.dart
+++ b/lib/pages/home_page.dart
@@ -13,10 +13,14 @@ import 'package:odin/painters/tooltip_painter.dart';
import 'package:odin/providers/file_notifier.dart';
import 'package:odin/services/locator.dart';
import 'package:odin/services/preferences_service.dart';
+import 'package:odin/services/shortener_service.dart';
import 'package:odin/services/toast_service.dart';
+import 'package:odin/widgets/mac_top_bar.dart';
import 'package:odin/widgets/window_top_bar.dart';
+import 'package:open_file/open_file.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
+import 'package:url_launcher/url_launcher.dart';
const backgroundStartColor = Color(0xFF7D5DEC);
const backgroundEndColor = Color(0xFF6148B9);
@@ -41,7 +45,7 @@ class _HomePageState extends State
bool _qrVisible = false;
final _toast = locator();
final PreferencesService _preferencesService = locator();
-
+ final TextEditingController _tokenController = TextEditingController();
@override
void initState() {
_preferencesService.init();
@@ -72,6 +76,7 @@ class _HomePageState extends State
@override
Widget build(BuildContext context) {
final _fileNotifier = context.watch();
+ final _shortenerService = locator();
return Scaffold(
backgroundColor: const Color(0xFF7D5DEC),
body: Container(
@@ -161,7 +166,7 @@ class _HomePageState extends State
: MediaQuery.of(context).size.width / 1.7),
width: _fileNotifier.processing
? 160
- : _fileNotifier.loading
+ : _fileNotifier.uploading || _fileNotifier.downloading
? 160
: 220,
height: 55,
@@ -174,11 +179,13 @@ class _HomePageState extends State
child: Text(
_fileNotifier.processing
? "Processing."
- : _fileNotifier.loading
+ : _fileNotifier.uploading
? "Uploading."
- : (Platform.isWindows || Platform.isMacOS)
- ? 'Drop files to start.'
- : 'Tap to share files.',
+ : _fileNotifier.downloading
+ ? "Downloading."
+ : (Platform.isWindows || Platform.isMacOS)
+ ? 'Drop files to start.'
+ : 'Tap to share files.',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 16,
@@ -219,7 +226,8 @@ class _HomePageState extends State
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Spacer(flex: 9),
- if (_fileNotifier.loading)
+ if (_fileNotifier.uploading ||
+ _fileNotifier.downloading)
if (_fileNotifier.zipfileName == '')
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
@@ -344,13 +352,20 @@ class _HomePageState extends State
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
- onTap: () => FlutterClipboard.copy(
- _fileNotifier.fileLink ?? '')
+ onTap: () => FlutterClipboard.copy(_fileNotifier
+ .fileLink !=
+ null
+ ? Platform.isAndroid || Platform.isIOS
+ ? "Some files were shared with you.\nTo access them, visit ${_fileNotifier.fileLink} from your mobile device. To access them on your PC, download Odin from https://shrtco.de/odin and enter this unique token - ${_shortenerService.token}"
+ : "Some files were shared with you.\nTo access them, download Odin from https://shrtco.de/odin and enter this unique token - ${_fileNotifier.fileLink}"
+ : '')
.then((value) => _toast.showToast(
Platform.isIOS || Platform.isMacOS
? CupertinoIcons.check_mark
: Icons.check,
- "Link copied to clipboard.")),
+ Platform.isIOS || Platform.isAndroid
+ ? "Link copied to clipboard."
+ : "Token copied to clipboard.")),
child: Container(
decoration: BoxDecoration(
color: Colors.white12,
@@ -433,10 +448,124 @@ class _HomePageState extends State
),
],
),
+ if (!_fileNotifier.processing &&
+ !_fileNotifier.uploading &&
+ !_fileNotifier.downloading &&
+ _fileNotifier.fileLink == null)
+ Text(
+ "or",
+ style: GoogleFonts.poppins(
+ fontSize: 12,
+ fontWeight: FontWeight.w200,
+ letterSpacing: 0.5,
+ color: Colors.white.withOpacity(0.5),
+ ),
+ ),
+ if (!_fileNotifier.processing &&
+ !_fileNotifier.uploading &&
+ !_fileNotifier.downloading &&
+ _fileNotifier.fileLink == null)
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white12,
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: Colors.white.withOpacity(0.05),
+ width: 0.5,
+ ),
+ ),
+ padding:
+ const EdgeInsets.fromLTRB(16, 0, 16, 0),
+ margin:
+ const EdgeInsets.fromLTRB(16, 16, 8, 16),
+ child: SizedBox(
+ width: (Platform.isWindows ||
+ Platform.isMacOS)
+ ? MediaQuery.of(context).size.width * 0.2
+ : MediaQuery.of(context).size.width * 0.4,
+ height: 44,
+ child: TextField(
+ controller: _tokenController,
+ style: GoogleFonts.poppins(
+ fontSize: 12,
+ fontWeight: FontWeight.w300,
+ letterSpacing: 0.5,
+ color: Colors.white.withOpacity(0.8),
+ ),
+ decoration: InputDecoration(
+ hintText: "Enter unique file token",
+ hintStyle: GoogleFonts.poppins(
+ fontSize: 12,
+ fontWeight: FontWeight.w200,
+ letterSpacing: 0.5,
+ color: Colors.white.withOpacity(0.7),
+ ),
+ border: InputBorder.none,
+ ),
+ ),
+ ),
+ ),
+ InkWell(
+ onTap: _tokenController.text.isNotEmpty &&
+ _tokenController.text.length > 16
+ ? () async {
+ final _filePath = await _fileNotifier
+ .getFileFromToken(
+ _tokenController.text.trim());
+ _tokenController.clear();
+ if (Platform.isWindows ||
+ Platform.isMacOS) {
+ launch(_filePath);
+ } else {
+ _toast.showMobileToast(
+ "Files saved in Downloads.");
+ await OpenFile.open(_filePath);
+ }
+ }
+ : null,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white12,
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: Colors.white.withOpacity(0.05),
+ width: 0.5,
+ ),
+ ),
+ padding:
+ const EdgeInsets.fromLTRB(10, 10, 10, 10),
+ margin:
+ const EdgeInsets.fromLTRB(8, 16, 16, 16),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ height: 24.0,
+ width: 24.0,
+ child: Icon(
+ Platform.isIOS || Platform.isMacOS
+ ? CupertinoIcons.qrcode
+ : Icons.adaptive
+ .arrow_forward_rounded,
+ size: 16,
+ color: Colors.white.withOpacity(0.8),
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
- "Files are encrypted with AES-256 encryption and will be deleted after 15 hours.",
+ _fileNotifier.fileLink != null
+ ? "Share this token with your friends to access the files."
+ : "Files are encrypted with AES-256 encryption and will be deleted after 15 hours.",
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: (Platform.isWindows || Platform.isMacOS)
@@ -551,7 +680,7 @@ class _HomePageState extends State
),
),
Text(
- _fileNotifier.fileLink.toString(),
+ "Scan using the Odin app to access the files.",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.normal,
@@ -567,8 +696,61 @@ class _HomePageState extends State
),
),
),
-
- const WindowTopBar(),
+ if (Platform.isWindows) const WindowTopBar(),
+ if (Platform.isMacOS) const MacTopBar(),
+ if (Platform.isAndroid || Platform.isIOS)
+ Align(
+ alignment: Alignment.topCenter,
+ child: SizedBox(
+ height: kToolbarHeight + MediaQuery.of(context).padding.top,
+ child: AppBar(
+ elevation: 0,
+ backgroundColor: Colors.transparent,
+ actions: [
+ // IconButton(
+ // onPressed: () {},
+ // icon: Icon(
+ // Platform.isIOS || Platform.isMacOS
+ // ? CupertinoIcons.qrcode_viewfinder
+ // : Icons.qr_code_scanner_rounded,
+ // color: Colors.white,
+ // ),
+ // ),
+ Theme(
+ data: ThemeData.dark(),
+ child: PopupMenuButton(
+ onSelected: (value) {
+ if (value == 1) {
+ launch('https://github.com/odinapp/odin#readme');
+ } else if (value == 2) {
+ launch(
+ 'https://www.buymeacoffee.com/HashStudios');
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ child: Text(
+ "About",
+ style: GoogleFonts.poppins(fontSize: 14),
+ ),
+ height: 40,
+ value: 1,
+ ),
+ PopupMenuItem(
+ child: Text(
+ "Support us",
+ style: GoogleFonts.poppins(fontSize: 14),
+ ),
+ value: 2,
+ height: 40,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
],
),
),
diff --git a/lib/providers/file_notifier.dart b/lib/providers/file_notifier.dart
index 8b14f35..7918775 100644
--- a/lib/providers/file_notifier.dart
+++ b/lib/providers/file_notifier.dart
@@ -4,7 +4,8 @@ import 'package:odin/services/locator.dart';
class FileNotifier with ChangeNotifier {
final _fileService = locator();
- bool get loading => _fileService.loading;
+ bool get uploading => _fileService.uploading;
+ bool get downloading => _fileService.downloading;
bool get processing => _fileService.processing;
String? get fileLink => _fileService.fileLink;
String get zipfileName => _fileService.zipfileName;
@@ -18,4 +19,10 @@ class FileNotifier with ChangeNotifier {
await _fileService.getLinkFromDroppedFiles(urls);
notifyListeners();
}
+
+ Future getFileFromToken(String token) async {
+ final _filePath = await _fileService.getFileFromToken(token);
+ notifyListeners();
+ return _filePath;
+ }
}
diff --git a/lib/services/download_service.dart b/lib/services/download_service.dart
new file mode 100644
index 0000000..37b0e91
--- /dev/null
+++ b/lib/services/download_service.dart
@@ -0,0 +1,57 @@
+import 'dart:io';
+
+import 'package:android_path_provider/android_path_provider.dart';
+import 'package:dio/dio.dart';
+import 'package:odin/services/logger.dart';
+import 'package:path/path.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+class DownloadService {
+ String progress = '0%';
+ bool downloading = false;
+
+ Dio dio = Dio();
+ Future _getFilePath(String fileName) async {
+ Directory? dir;
+ if (Platform.isAndroid) {
+ var status = await Permission.storage.status;
+ if (!status.isGranted) {
+ await Permission.storage.request();
+ }
+ dir = Directory(await AndroidPathProvider.downloadsPath);
+ } else if (Platform.isIOS) {
+ dir = await getTemporaryDirectory();
+ } else {
+ dir = await getDownloadsDirectory();
+ }
+ String path = join(dir?.path ?? '', fileName);
+ logger.d("File path : $path");
+ return path;
+ }
+
+ Future downloadFile(String url) async {
+ progress = '0%';
+ downloading = true;
+ Response response = await Dio().get(
+ url,
+ options: Options(
+ followRedirects: false,
+ validateStatus: (status) {
+ return (status ?? 0) < 500;
+ }),
+ );
+ final filePath = await _getFilePath(
+ basename(response.headers.map["location"]?[0] ?? ''));
+ await dio.download(
+ url,
+ filePath,
+ onReceiveProgress: (rcv, total) {
+ progress = ((rcv / total) * 100).toStringAsFixed(0) + "%";
+ },
+ deleteOnError: true,
+ );
+ downloading = false;
+ return File(filePath);
+ }
+}
diff --git a/lib/services/encryption_service.dart b/lib/services/encryption_service.dart
new file mode 100644
index 0000000..906c205
--- /dev/null
+++ b/lib/services/encryption_service.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+
+import 'package:aes_crypt_null_safe/aes_crypt_null_safe.dart';
+import 'package:odin/services/locator.dart';
+import 'package:odin/services/logger.dart';
+import 'package:odin/services/random_service.dart';
+import 'package:path/path.dart';
+
+class EncryptionService {
+ final RandomService _randomService = locator();
+
+ Future