diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d779c3f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,119 @@
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
+!default.xcworkspace
+*.dSYM
+*.dSYM.zip
+*.hmap
+*.ipa
+*.lcov
+*.lock
+*.log
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+*.pid
+*.pid.lock
+*.seed
+*.swp
+*.tgz
+*.tsbuildinfo
+*.xccheckout
+*.xcscmblueprint
+*.xcuserstate
+*~.nib
+.AppleDB
+.AppleDesktop
+.AppleDouble
+.DS_Store
+.DocumentRevisions-V100
+.LSOverride
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+._*
+.apdisk
+.build
+.bundle
+.cache
+.cache/
+.com.apple.timemachine.donotpresent
+.dynamodb/
+.env
+.env.test
+.eslintcache
+.fseventsd
+.fusebox/
+.grunt
+.idea
+.lock-wscript
+.next
+.node_repl_history
+.npm
+.nuxt
+.nyc_output
+.parcel-cache
+.pnp.*
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+.serverless/
+.swiftpm
+.tern-port
+.vscode-test
+.vuepress/dist
+.yarn-integrity
+.yarn/build-state.yml
+.yarn/cache
+.yarn/unplugged
+/*.gcno
+Artifacts/
+CI
+CI-Pods.tar
+Carthage/Build
+Carthage/Build/
+DerivedData
+DerivedData/
+Icon
+Network Trash Folder
+Pipeline/Dockers/Buildtime/
+Podfile.lock
+Pods/
+Temporary Items
+artifacts/
+bower_components
+build/
+build/Release
+coverage
+default.profraw
+dist
+dockerbuild
+dockermnt
+fastlane/Preview.html
+fastlane/report.xml
+fastlane/screenshots/**/*.png
+fastlane/test_output
+iOSInjectionProject/
+jspm_packages/
+lerna-debug.log*
+lib-cov
+logs
+node_modules/
+npm-debug.log*
+pids
+profile
+project.xcworkspace
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+temp/
+temps/
+web_modules/
+xcuserdata
+xcuserdata/
+yarn-debug.log*
+yarn-error.log*
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f188537
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Lakr Aream
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Preview.png b/Preview.png
new file mode 100644
index 0000000..037341d
Binary files /dev/null and b/Preview.png differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..51a0089
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+# Insight
+
+Read iOS 15 privacy insight '.ndjson' file into your human brain. Written in SwiftUI.
+
+![Preview](./Preview.png)
+
+## Feature
+
+- [x] Compile records into app summary
+- [x] Relink app info from App Store
+- [ ] Filter & Search
+- [ ] Export Report
+- [ ] Embed SwiftCharts
+
+## Disclaimer
+
+This app reimplements the functionality from @laosbxd [(see more)](https://twitter.com/laosbxd/status/1440375461477949442), but I wrote the code on my own for research purpose.
+
+## License
+
+Insight is licensed under [MIT](./LICENSE). App library license are included in their own git repository.
+
+---
+
+Copyright © 2021 Lakr Aream. All Rights Reserved.
\ No newline at end of file
diff --git a/insight.xcodeproj/project.pbxproj b/insight.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..cea49e2
--- /dev/null
+++ b/insight.xcodeproj/project.pbxproj
@@ -0,0 +1,401 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 55;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		507B2E212701EA230079B57C /* Colorful in Frameworks */ = {isa = PBXBuildFile; productRef = 507B2E202701EA230079B57C /* Colorful */; };
+		507B2E232701F1850079B57C /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507B2E222701F1850079B57C /* DataModel.swift */; };
+		507B2E252701FA800079B57C /* AppSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507B2E242701FA800079B57C /* AppSelector.swift */; };
+		507B2E282702060B0079B57C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 507B2E272702060B0079B57C /* Kingfisher */; };
+		507B2E2A270212B40079B57C /* AppDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507B2E29270212B40079B57C /* AppDetail.swift */; };
+		50A187512701E707003E32EF /* insightApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A187502701E707003E32EF /* insightApp.swift */; };
+		50A187532701E707003E32EF /* insightMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A187522701E707003E32EF /* insightMain.swift */; };
+		50A187552701E708003E32EF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A187542701E708003E32EF /* Assets.xcassets */; };
+		50A187582701E708003E32EF /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A187572701E708003E32EF /* Preview Assets.xcassets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		507B2E222701F1850079B57C /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModel.swift; sourceTree = "<group>"; };
+		507B2E242701FA800079B57C /* AppSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelector.swift; sourceTree = "<group>"; };
+		507B2E29270212B40079B57C /* AppDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetail.swift; sourceTree = "<group>"; };
+		50A1874D2701E707003E32EF /* insight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = insight.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		50A187502701E707003E32EF /* insightApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = insightApp.swift; sourceTree = "<group>"; };
+		50A187522701E707003E32EF /* insightMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = insightMain.swift; sourceTree = "<group>"; };
+		50A187542701E708003E32EF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		50A187572701E708003E32EF /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+		50A187592701E708003E32EF /* insight.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = insight.entitlements; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		50A1874A2701E707003E32EF /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				507B2E282702060B0079B57C /* Kingfisher in Frameworks */,
+				507B2E212701EA230079B57C /* Colorful in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		50A187442701E707003E32EF = {
+			isa = PBXGroup;
+			children = (
+				50A1874F2701E707003E32EF /* insight */,
+				50A1874E2701E707003E32EF /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		50A1874E2701E707003E32EF /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				50A1874D2701E707003E32EF /* insight.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		50A1874F2701E707003E32EF /* insight */ = {
+			isa = PBXGroup;
+			children = (
+				50A187502701E707003E32EF /* insightApp.swift */,
+				50A187522701E707003E32EF /* insightMain.swift */,
+				507B2E242701FA800079B57C /* AppSelector.swift */,
+				507B2E29270212B40079B57C /* AppDetail.swift */,
+				507B2E222701F1850079B57C /* DataModel.swift */,
+				50A187542701E708003E32EF /* Assets.xcassets */,
+				50A187592701E708003E32EF /* insight.entitlements */,
+				50A187562701E708003E32EF /* Preview Content */,
+			);
+			path = insight;
+			sourceTree = "<group>";
+		};
+		50A187562701E708003E32EF /* Preview Content */ = {
+			isa = PBXGroup;
+			children = (
+				50A187572701E708003E32EF /* Preview Assets.xcassets */,
+			);
+			path = "Preview Content";
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		50A1874C2701E707003E32EF /* insight */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 50A1875C2701E708003E32EF /* Build configuration list for PBXNativeTarget "insight" */;
+			buildPhases = (
+				50A187492701E707003E32EF /* Sources */,
+				50A1874A2701E707003E32EF /* Frameworks */,
+				50A1874B2701E707003E32EF /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = insight;
+			packageProductDependencies = (
+				507B2E202701EA230079B57C /* Colorful */,
+				507B2E272702060B0079B57C /* Kingfisher */,
+			);
+			productName = insight;
+			productReference = 50A1874D2701E707003E32EF /* insight.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		50A187452701E707003E32EF /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1300;
+				LastUpgradeCheck = 1300;
+				TargetAttributes = {
+					50A1874C2701E707003E32EF = {
+						CreatedOnToolsVersion = 13.0;
+					};
+				};
+			};
+			buildConfigurationList = 50A187482701E707003E32EF /* Build configuration list for PBXProject "insight" */;
+			compatibilityVersion = "Xcode 13.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 50A187442701E707003E32EF;
+			packageReferences = (
+				507B2E1F2701EA230079B57C /* XCRemoteSwiftPackageReference "Colorful" */,
+				507B2E262702060B0079B57C /* XCRemoteSwiftPackageReference "Kingfisher" */,
+			);
+			productRefGroup = 50A1874E2701E707003E32EF /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				50A1874C2701E707003E32EF /* insight */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		50A1874B2701E707003E32EF /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				50A187582701E708003E32EF /* Preview Assets.xcassets in Resources */,
+				50A187552701E708003E32EF /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		50A187492701E707003E32EF /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				507B2E232701F1850079B57C /* DataModel.swift in Sources */,
+				507B2E2A270212B40079B57C /* AppDetail.swift in Sources */,
+				50A187532701E707003E32EF /* insightMain.swift in Sources */,
+				507B2E252701FA800079B57C /* AppSelector.swift in Sources */,
+				50A187512701E707003E32EF /* insightApp.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		50A1875A2701E708003E32EF /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 11.3;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		50A1875B2701E708003E32EF /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 11.3;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Release;
+		};
+		50A1875D2701E708003E32EF /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = insight/insight.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				CURRENT_PROJECT_VERSION = 2;
+				DEVELOPMENT_ASSET_PATHS = "\"insight/Preview Content\"";
+				ENABLE_HARDENED_RUNTIME = YES;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				MACOSX_DEPLOYMENT_TARGET = 11.0;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.insight;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		50A1875E2701E708003E32EF /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = insight/insight.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				CURRENT_PROJECT_VERSION = 2;
+				DEVELOPMENT_ASSET_PATHS = "\"insight/Preview Content\"";
+				ENABLE_HARDENED_RUNTIME = YES;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				MACOSX_DEPLOYMENT_TARGET = 11.0;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.insight;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		50A187482701E707003E32EF /* Build configuration list for PBXProject "insight" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				50A1875A2701E708003E32EF /* Debug */,
+				50A1875B2701E708003E32EF /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		50A1875C2701E708003E32EF /* Build configuration list for PBXNativeTarget "insight" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				50A1875D2701E708003E32EF /* Debug */,
+				50A1875E2701E708003E32EF /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+		507B2E1F2701EA230079B57C /* XCRemoteSwiftPackageReference "Colorful" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/Co2333/Colorful";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.0.0;
+			};
+		};
+		507B2E262702060B0079B57C /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/onevcat/Kingfisher";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 7.0.0;
+			};
+		};
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		507B2E202701EA230079B57C /* Colorful */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 507B2E1F2701EA230079B57C /* XCRemoteSwiftPackageReference "Colorful" */;
+			productName = Colorful;
+		};
+		507B2E272702060B0079B57C /* Kingfisher */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 507B2E262702060B0079B57C /* XCRemoteSwiftPackageReference "Kingfisher" */;
+			productName = Kingfisher;
+		};
+/* End XCSwiftPackageProductDependency section */
+	};
+	rootObject = 50A187452701E707003E32EF /* Project object */;
+}
diff --git a/insight/AppDetail.swift b/insight/AppDetail.swift
new file mode 100644
index 0000000..3547cce
--- /dev/null
+++ b/insight/AppDetail.swift
@@ -0,0 +1,102 @@
+//
+//  Detail.swift
+//  insight
+//
+//  Created by Lakr Aream on 2021/9/27.
+//
+
+import SwiftUI
+
+private let formatter: ISO8601DateFormatter = {
+    let formatter = ISO8601DateFormatter()
+    formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+    return formatter
+}()
+
+private func localizedDate(from8601Date str: String) -> String {
+    guard let date = formatter.date(from: str) else {
+        return "Unknown Date"
+    }
+    return DateFormatter.localizedString(from: date,
+                                         dateStyle: .long,
+                                         timeStyle: .long)
+}
+
+struct AppDetailView: View {
+    @Binding var report: NDPrivacySummary.NDApplicationSummary?
+
+    var body: some View {
+        if let report = report {
+            VStack(alignment: .leading) {
+                HStack(alignment: .bottom) {
+                    Image(systemName: "magnifyingglass.circle.fill")
+                        .font(.system(size: 24, weight: .semibold, design: .rounded))
+                        .foregroundColor(.pink)
+                    Text(report.bundleIdentifier)
+                        .font(.system(size: 24, weight: .semibold, design: .rounded))
+                    Spacer()
+                }
+                Divider()
+                if report.reportPrivacyElement.count > 0 {
+                    Text("Privacy Timeline")
+                        .font(.system(size: 14, weight: .semibold, design: .default))
+                    ForEach(report.reportPrivacyElement, id: \.self) { privacyElement in
+                        HStack(spacing: 8) {
+                            Circle()
+                                .foregroundColor(.red)
+                                .frame(width: 6, height: 6)
+                            VStack(alignment: .leading, spacing: 4) {
+                                HStack(spacing: 8) {
+                                    Group {
+                                        if privacyElement.category.lowercased() == "location" {
+                                            Image(systemName: "location.circle.fill")
+                                        } else if privacyElement.category.lowercased() == "photos" {
+                                            Image(systemName: "photo.fill.on.rectangle.fill")
+                                        } else if privacyElement.category.lowercased() == "contacts" {
+                                            Image(systemName: "person.2.circle.fill")
+                                        } else if privacyElement.category.lowercased() == "camera" {
+                                            Image(systemName: "camera.circle.fill")
+                                        } else if privacyElement.category.lowercased() == "microphone" {
+                                            Image(systemName: "mic.circle.fill")
+                                        }
+                                    }
+                                    .foregroundColor(.pink)
+                                    .frame(width: 10)
+                                    .font(.system(size: 12, weight: .semibold, design: .default))
+                                    Text(privacyElement.category.uppercased())
+                                        .font(.system(size: 12, weight: .semibold, design: .default))
+                                }
+                                Text(localizedDate(from8601Date: privacyElement.timeStamp))
+                                    .font(.system(size: 8, weight: .semibold, design: .monospaced))
+                            }
+                        }
+                        .padding(.vertical, 4)
+                    }
+                }
+                if report.reportNetworkElement.count > 0 {
+                    Text("Network Timeline")
+                        .font(.system(size: 14, weight: .semibold, design: .default))
+                    ForEach(report.reportNetworkElement, id: \.self) { networkElement in
+                        HStack(spacing: 8) {
+                            Circle()
+                                .foregroundColor(.red)
+                                .frame(width: 6, height: 6)
+                            VStack(alignment: .leading, spacing: 4) {
+                                HStack(spacing: 8) {
+                                    Text(networkElement.domain)
+                                        .font(.system(size: 12, weight: .semibold, design: .monospaced))
+                                }
+                                Text(localizedDate(from8601Date: networkElement.timeStamp))
+                                    .font(.system(size: 8, weight: .semibold, design: .monospaced))
+                            }
+                        }
+                        .padding(.vertical, 4)
+                    }
+                }
+            }
+            .padding()
+        } else {
+            ZStack {}
+        }
+    }
+}
diff --git a/insight/AppSelector.swift b/insight/AppSelector.swift
new file mode 100644
index 0000000..0181cbb
--- /dev/null
+++ b/insight/AppSelector.swift
@@ -0,0 +1,233 @@
+//
+//  Reader.swift
+//  insight
+//
+//  Created by Lakr Aream on 2021/9/27.
+//
+
+import AVFAudio
+import Colorful
+import Kingfisher
+import SwiftUI
+
+struct InsightReaderView: View {
+    let insightReport: NDPrivacySummary
+
+    @State var appKeys: [String] = []
+    @State var hideApple: Bool = true
+    @State var selectedApplication: NDPrivacySummary.NDApplicationSummary?
+    @State var highlightIndex: Int?
+
+    var body: some View {
+        GeometryReader { reader in
+            VStack(spacing: 0) {
+                HStack(spacing: 0) {
+                    VStack(alignment: .trailing, spacing: 0) {
+                        Text(generateHeader())
+                            .font(.system(size: 10, weight: .semibold, design: .monospaced))
+                            .padding(.horizontal)
+                    }
+                    Toggle("Hide Apple", isOn: $hideApple)
+                }
+                .frame(width: reader.size.width, height: 30)
+                Divider()
+                GeometryReader { innerReader in
+                    HStack(spacing: 0) {
+                        ScrollView {
+                            VStack {
+                                ForEach(appKeys, id: \.self) { key in
+                                    if let idx = appKeys.firstIndex(of: key),
+                                       let app = insightReport.applicationSummary[key]
+                                    {
+                                        ApplicationView(app: app)
+                                            .padding(4)
+                                            .onHover { hover in
+                                                highlightIndex = hover ? idx : nil
+                                            }
+                                            .scaleEffect(idx == highlightIndex ? 1.02 : 1)
+                                            .background(
+                                                Color
+                                                    .yellow
+                                                    .opacity(idx == highlightIndex ? 0.2 : 0)
+                                                    .cornerRadius(8)
+                                            )
+                                            .animation(.interactiveSpring(), value: highlightIndex)
+                                            .onTapGesture {
+                                                selectedApplication = app
+                                            }
+                                            .padding(.horizontal, 8)
+                                        Divider()
+                                    }
+                                }
+                            }
+                            .padding(.vertical, 20)
+                        }
+                        .frame(width: innerReader.size.width * 0.35)
+                        Divider()
+                        ScrollView {
+                            if selectedApplication != nil {
+                                AppDetailView(report: $selectedApplication)
+                                    .animation(.interactiveSpring(), value: selectedApplication)
+                            } else {
+                                ZStack {
+                                    VStack(spacing: 8) {
+                                        Image(systemName: "cursorarrow.rays")
+                                            .font(.system(size: 60, weight: .semibold, design: .rounded))
+                                            .foregroundColor(.pink)
+                                        Text("Select an app to begin analyze ~")
+                                            .font(.system(size: 16, weight: .semibold, design: .rounded))
+                                    }
+                                }
+                                .frame(width: innerReader.size.width * 0.65, height: innerReader.size.height)
+                            }
+                        }
+                        .frame(width: innerReader.size.width * 0.65)
+                    }
+                }
+            }
+            .frame(width: reader.size.width, height: reader.size.height)
+        }
+        .onChange(of: hideApple, perform: { _ in
+            rebuildKeys()
+        })
+        .onAppear {
+            rebuildKeys()
+        }
+        .background(Color(NSColor.textBackgroundColor))
+        .ignoresSafeArea()
+    }
+
+    func rebuildKeys() {
+        let origKeys = insightReport
+            .applicationSummary
+            .keys
+            .sorted()
+        if hideApple {
+            debugPrint("hide apple")
+            appKeys = origKeys
+                .filter { !$0.lowercased().hasPrefix("com.apple") }
+        } else {
+            debugPrint("unhide apple")
+            appKeys = origKeys
+        }
+    }
+
+    func generateHeader() -> String {
+        "[Insight]" +
+//        " - " +
+//        "Privacy " + String(insightReport.privacyAccess.count) +
+//        " " +
+//        "Network " + String(insightReport.networkAccess.count) +
+            " " +
+            generateRecordRange()
+    }
+
+    func generateRecordRange() -> String {
+        DateFormatter.localizedString(from: insightReport.beginDate,
+                                      dateStyle: .medium,
+                                      timeStyle: .medium)
+            + " -> " +
+            DateFormatter.localizedString(from: insightReport.endingDate,
+                                          dateStyle: .medium,
+                                          timeStyle: .medium)
+    }
+}
+
+struct ApplicationView: View {
+    let instructedHeight: CGFloat = 35
+    
+    @State var app: NDPrivacySummary.NDApplicationSummary
+    @State var avatarImage: KFImage?
+    @State var appName: String?
+    
+    var body: some View {
+        GeometryReader { _ in
+            HStack {
+                ZStack {
+                    if let avatarImage = avatarImage {
+                        avatarImage
+                            .resizable()
+                            .aspectRatio(contentMode: .fit)
+                            .frame(width: instructedHeight * 0.75, height: instructedHeight * 0.75)
+                            .cornerRadius(8)
+                    } else {
+                        Image("AppStore")
+                            .resizable()
+                            .aspectRatio(contentMode: .fit)
+                            .frame(width: instructedHeight * 0.75, height: instructedHeight * 0.75)
+                            .cornerRadius(8)
+                            .foregroundColor(.pink)
+                    }
+                }
+                .frame(width: instructedHeight, height: instructedHeight)
+                VStack(alignment: .leading, spacing: 6) {
+                    Text(appName ?? "[? Application]")
+                        .font(.system(size: 14, weight: .semibold, design: .rounded))
+                    HStack(spacing: 0) {
+                        Text(app.bundleIdentifier)
+                            .minimumScaleFactor(0.5)
+                        Spacer()
+                        HStack(spacing: 2) {
+                            Image(systemName: "hand.raised.fill")
+                                .font(.system(size: 8, weight: .regular, design: .rounded))
+                            Text("\(app.reportPrivacyElement.count)")
+                                .minimumScaleFactor(0.5)
+                            Spacer()
+                        }
+                        .frame(width: 40)
+                        .foregroundColor(.red)
+                        HStack(spacing: 2) {
+                            Image(systemName: "network")
+                                .font(.system(size: 8, weight: .regular, design: .rounded))
+                            Text("\(app.reportNetworkElement.count)")
+                                .minimumScaleFactor(0.5)
+                            Spacer()
+                        }
+                        .frame(width: 40)
+                        .foregroundColor(.blue)
+                    }
+                    .font(.system(size: 10, weight: .regular, design: .monospaced))
+                }
+            }
+        }
+        .frame(height: instructedHeight)
+        .onAppear {
+            prepareApplicationInfo()
+        }
+    }
+
+    func prepareApplicationInfo() {
+        appName = nil
+        avatarImage = nil
+        DispatchQueue.global().async {
+            guard let queryIdUrl = URL(string: "https://itunes.apple.com/lookup?bundleId=\(app.bundleIdentifier)") else {
+                return
+            }
+            if let cache = appStoreQueryCache[queryIdUrl],
+               let apiResult = try? ASAPIResult(cache).results?.first {
+                debugPrint("[i] Cached application \(app.bundleIdentifier) => \(apiResult.trackName ?? "nope!")")
+                applyAppStoreApiResult(object: apiResult)
+                return
+            }
+            URLSession
+                .shared
+                .dataTask(with: queryIdUrl) { data, _, _ in
+                    if let data = data,
+                       let str = String(data: data, encoding: .utf8),
+                       let apiResult = try? ASAPIResult(str).results?.first {
+                        appStoreQueryCache[queryIdUrl] = str
+                        debugPrint("[i] Loaded application \(app.bundleIdentifier) => \(apiResult.trackName ?? "nope!")")
+                        applyAppStoreApiResult(object: apiResult)
+                    }
+                }
+                .resume()
+        }
+    }
+
+    func applyAppStoreApiResult(object: ASResult) {
+        DispatchQueue.main.async {
+            appName = object.trackName
+            avatarImage = KFImage(URL(string: object.artworkUrl60 ?? ""))
+        }
+    }
+}
diff --git a/insight/Assets.xcassets/AccentColor.colorset/Contents.json b/insight/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/insight/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/Contents.json b/insight/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..0a856ef
--- /dev/null
+++ b/insight/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+    "images": [
+        {
+            "size": "16x16",
+            "idiom": "mac",
+            "filename": "icon-16.png",
+            "scale": "1x"
+        },
+        {
+            "size": "16x16",
+            "idiom": "mac",
+            "filename": "icon-16@2x.png",
+            "scale": "2x"
+        },
+        {
+            "size": "32x32",
+            "idiom": "mac",
+            "filename": "icon-32.png",
+            "scale": "1x"
+        },
+        {
+            "size": "32x32",
+            "idiom": "mac",
+            "filename": "icon-32@2x.png",
+            "scale": "2x"
+        },
+        {
+            "size": "128x128",
+            "idiom": "mac",
+            "filename": "icon-128.png",
+            "scale": "1x"
+        },
+        {
+            "size": "128x128",
+            "idiom": "mac",
+            "filename": "icon-128@2x.png",
+            "scale": "2x"
+        },
+        {
+            "size": "256x256",
+            "idiom": "mac",
+            "filename": "icon-256.png",
+            "scale": "1x"
+        },
+        {
+            "size": "256x256",
+            "idiom": "mac",
+            "filename": "icon-256@2x.png",
+            "scale": "2x"
+        },
+        {
+            "size": "512x512",
+            "idiom": "mac",
+            "filename": "icon-512.png",
+            "scale": "1x"
+        },
+        {
+            "size": "512x512",
+            "idiom": "mac",
+            "filename": "icon-512@2x.png",
+            "scale": "2x"
+        }
+    ],
+    "info": {
+        "version": 1,
+        "author": "icon.wuruihong.com"
+    }
+}
\ No newline at end of file
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-128.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-128.png
new file mode 100644
index 0000000..8a7db86
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-128.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png
new file mode 100644
index 0000000..68a4bec
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-16.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-16.png
new file mode 100644
index 0000000..63b49a4
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-16.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png
new file mode 100644
index 0000000..da4c7db
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-256.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-256.png
new file mode 100644
index 0000000..68a4bec
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-256.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png
new file mode 100644
index 0000000..4e18c84
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-32.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-32.png
new file mode 100644
index 0000000..da4c7db
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-32.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png
new file mode 100644
index 0000000..f610833
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-512.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-512.png
new file mode 100644
index 0000000..4e18c84
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-512.png differ
diff --git a/insight/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png b/insight/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png
new file mode 100644
index 0000000..af52d80
Binary files /dev/null and b/insight/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png differ
diff --git a/insight/Assets.xcassets/AppStore.imageset/Contents.json b/insight/Assets.xcassets/AppStore.imageset/Contents.json
new file mode 100644
index 0000000..fa5ed5e
--- /dev/null
+++ b/insight/Assets.xcassets/AppStore.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+  "images" : [
+    {
+      "filename" : "app_store.svg",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "template-rendering-intent" : "template"
+  }
+}
diff --git a/insight/Assets.xcassets/AppStore.imageset/app_store.svg b/insight/Assets.xcassets/AppStore.imageset/app_store.svg
new file mode 100644
index 0000000..589ab6d
--- /dev/null
+++ b/insight/Assets.xcassets/AppStore.imageset/app_store.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg   viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
+    <title>ic_fluent_app_store_24_filled</title>
+    <desc>Created with Sketch.</desc>
+    <g id="🔍-Product-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="ic_fluent_app_store_24_filled" fill="#212121" fill-rule="nonzero">
+            <path d="M17.75,3 C19.5449254,3 21,4.45507456 21,6.25 L21,17.75 C21,19.5449254 19.5449254,21 17.75,21 L6.25,21 C4.45507456,21 3,19.5449254 3,17.75 L3,6.25 C3,4.45507456 4.45507456,3 6.25,3 L17.75,3 Z M9.36866957,15.2541207 L7.63066957,15.2541207 L7.56419595,15.3696993 L7.5187736,15.4610282 C7.37700209,15.8021724 7.50562161,16.2040885 7.83345734,16.395642 C8.16132045,16.587148 8.57459569,16.5018069 8.80214266,16.2107727 L8.85939238,16.1263533 L9.36866957,15.2541207 Z M13.2925743,10.0240744 L12.4215743,11.5160744 L15.079611,16.1237835 L15.1363996,16.2085136 C15.3623565,16.5007826 15.7751611,16.5883646 16.1040459,16.3986183 C16.4329304,16.2088714 16.5637317,15.8076616 16.4238081,15.4657566 L16.3788804,15.3741835 L15.8765743,14.5040744 L16.7518937,14.504946 L16.8536642,14.4980994 C17.1864602,14.4529518 17.4498994,14.1895126 17.495047,13.8567166 L17.5018937,13.754946 L17.495047,13.6531755 C17.4498994,13.3203795 17.1864602,13.0569403 16.8536642,13.0117927 L16.7518937,13.004946 L15.0115743,13.0040744 L13.2925743,10.0240744 Z M13.2631232,7.10145556 C12.9352692,6.90993382 12.5219947,6.99527355 12.2944542,7.28631143 L12.2372064,7.37073189 L11.9965416,7.7799285 L11.7634041,7.37425403 L11.7066156,7.28952396 C11.5032545,7.02648174 11.1485467,6.9292361 10.8398818,7.05074264 L10.7389696,7.09941939 L10.6542395,7.15620792 C10.3911973,7.35956896 10.2939517,7.71427678 10.4154582,8.02294165 L10.464135,8.12385388 L11.1265416,9.2709285 L8.94557613,13.0049285 L7.25157613,13.004946 L7.14980557,13.0117927 C6.78373001,13.0614551 6.50157613,13.3752503 6.50157613,13.754946 C6.50157613,14.1346418 6.78373001,14.448437 7.14980557,14.4980994 L7.25157613,14.504946 L13.279,14.504803 L12.414,13.004803 L10.6825761,13.0039285 L13.5323945,8.12738105 L13.5778169,8.03605217 C13.7195883,7.69490804 13.590968,7.29299325 13.2631232,7.10145556 Z" id="🎨-Color"></path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/insight/Assets.xcassets/Contents.json b/insight/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/insight/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/insight/Assets.xcassets/RoundedIcon.imageset/Contents.json b/insight/Assets.xcassets/RoundedIcon.imageset/Contents.json
new file mode 100644
index 0000000..05b8bc2
--- /dev/null
+++ b/insight/Assets.xcassets/RoundedIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "RoundedIcon.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/insight/Assets.xcassets/RoundedIcon.imageset/RoundedIcon.png b/insight/Assets.xcassets/RoundedIcon.imageset/RoundedIcon.png
new file mode 100644
index 0000000..7cc2609
Binary files /dev/null and b/insight/Assets.xcassets/RoundedIcon.imageset/RoundedIcon.png differ
diff --git a/insight/DataModel.swift b/insight/DataModel.swift
new file mode 100644
index 0000000..1bcb5ac
--- /dev/null
+++ b/insight/DataModel.swift
@@ -0,0 +1,983 @@
+// This file was generated from JSON Schema using quicktype, do not modify it directly.
+// To parse the JSON, add this file to your project and do:
+//
+//   let NDPrivacyAccess = try NDPrivacyAccess(json)
+
+//
+// Hashable or Equatable:
+// The compiler will not be able to synthesize the implementation of Hashable or Equatable
+// for types that require the use of JSONAny, nor will the implementation of Hashable be
+// synthesized for types that have collections (such as arrays or dictionaries).
+
+import Foundation
+
+public struct NDPrivacySummary {
+    let privacyAccess: [NDPrivacyAccess]
+    let networkAccess: [NDNetworkAccess]
+    let applicationSummary: [String: NDApplicationSummary]
+    let beginDate: Date
+    let endingDate: Date
+
+    public struct NDApplicationSummary: Equatable, Identifiable {
+        public var id = UUID()
+        let bundleIdentifier: String
+        let reportPrivacyElement: [NDPrivacyAccess]
+        let reportNetworkElement: [NDNetworkAccess]
+    }
+
+    private class NDApplicationSummaryBuilder {
+        let bundleIdentifier: String
+        var reportPrivacyElement: [NDPrivacyAccess] = []
+        var reportNetworkElement: [NDNetworkAccess] = []
+
+        init(bundleIdentifier: String) {
+            self.bundleIdentifier = bundleIdentifier
+        }
+
+        func lockdown() -> NDApplicationSummary {
+            let formatter = ISO8601DateFormatter()
+            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+            return .init(bundleIdentifier: bundleIdentifier,
+                         reportPrivacyElement: reportPrivacyElement
+                             .sorted(by: { a, b in
+                                 guard let dateA = formatter.date(from: a.timeStamp),
+                                       let dateB = formatter.date(from: b.timeStamp)
+                                 else {
+                                     return false
+                                 }
+                                 return dateA < dateB
+                             }),
+                         reportNetworkElement: reportNetworkElement
+                             .sorted(by: { a, b in
+                                 guard let dateA = formatter.date(from: a.timeStamp),
+                                       let dateB = formatter.date(from: b.timeStamp)
+                                 else {
+                                     return false
+                                 }
+                                 return dateA < dateB
+                             })
+            )
+        }
+    }
+
+    typealias TotalAndCurrent = (Int, Int)
+    init(
+        privacyAccess: [NDPrivacyAccess],
+        networkAccess: [NDNetworkAccess],
+        progressUpdate: (TotalAndCurrent) -> Void
+    ) {
+        var total = privacyAccess.count + networkAccess.count
+        self.privacyAccess = privacyAccess
+        self.networkAccess = networkAccess
+        var constructor: [String: NDApplicationSummaryBuilder] = [:]
+        var begin: Date = Date(timeIntervalSince1970: 2147483647000)
+        var end: Date = Date(timeIntervalSince1970: 0)
+        var complete: Int = 0
+        privacyAccess.forEach { access in
+            /*
+             {
+                 "accessor": {
+                     "identifier": "com.bytedance.ee.lark",
+                     "identifierType": "bundleID"
+                 },
+                 "category": "camera",
+                 "identifier": "6279DD3B-9B63-4ED4-86C8-F2CD0E9E582D",
+                 "kind": "intervalBegin",
+                 "timeStamp": "2021-09-21T17:08:41.458+08:00",
+                 "type": "access"
+             }
+             */
+            let identifier = access.accessor.identifier
+            guard identifier.count > 0 else { return }
+            let read = constructor[identifier, default: .init(bundleIdentifier: identifier)]
+            read.reportPrivacyElement.append(access)
+            constructor[identifier] = read
+            let formatter = ISO8601DateFormatter()
+            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+            if let currentDate = formatter.date(from: access.timeStamp) {
+                if currentDate.timeIntervalSince(begin) < 0 { begin = currentDate }
+                if currentDate.timeIntervalSince(end) > 0 { end = currentDate }
+            }
+            progressUpdate((total, complete))
+            complete += 1
+        }
+        networkAccess.forEach { access in
+            /*
+             {
+                 "domain": "61.174.42.248",
+                 "firstTimeStamp": "2021-09-27T00:36:26.432+08:00",
+                 "context": "",
+                 "timeStamp": "2021-09-27T00:36:26.432+08:00",
+                 "domainType": 2,
+                 "initiatedType": "AppInitiated",
+                 "hits": 1,
+                 "type": "networkActivity",
+                 "domainOwner": "",
+                 "bundleID": "com.nssurge.inc.surge-ios"
+             }
+             */
+            let identifier = access.bundleid
+            guard identifier.count > 0 else { return }
+            let read = constructor[identifier, default: .init(bundleIdentifier: identifier)]
+            read.reportNetworkElement.append(access)
+            constructor[identifier] = read
+            let formatter = ISO8601DateFormatter()
+            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+            if let currentDate = formatter.date(from: access.timeStamp) {
+                if currentDate.timeIntervalSince(begin) < 0 { begin = currentDate }
+                if currentDate.timeIntervalSince(end) > 0 { end = currentDate }
+            }
+            progressUpdate((total, complete))
+            complete += 1
+        }
+        var summaryBuilder = [String: NDApplicationSummary]()
+        total = constructor.count
+        complete = 0
+        for (key, value) in constructor {
+            summaryBuilder[key] = value.lockdown()
+            complete += 1
+            progressUpdate((total, complete))
+        }
+        applicationSummary = summaryBuilder
+        beginDate = begin
+        endingDate = end
+    }
+}
+
+// MARK: - NDPrivacyAccess
+
+public struct NDPrivacyAccess: Codable, Hashable {
+    public let accessor: NDAccessor
+    public let category: String
+    public let identifier: String
+    public let kind: String
+    public let timeStamp: String
+    public let type: String
+
+    enum CodingKeys: String, CodingKey {
+        case accessor
+        case category
+        case identifier
+        case kind
+        case timeStamp
+        case type
+    }
+
+    public init(accessor: NDAccessor, category: String, identifier: String, kind: String, timeStamp: String, type: String) {
+        self.accessor = accessor
+        self.category = category
+        self.identifier = identifier
+        self.kind = kind
+        self.timeStamp = timeStamp
+        self.type = type
+    }
+}
+
+// MARK: NDPrivacyAccess convenience initializers and mutators
+
+public extension NDPrivacyAccess {
+    init(data: Data) throws {
+        self = try newJSONDecoder().decode(NDPrivacyAccess.self, from: data)
+    }
+
+    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
+        guard let data = json.data(using: encoding) else {
+            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
+        }
+        try self.init(data: data)
+    }
+
+    init(fromURL url: URL) throws {
+        try self.init(data: try Data(contentsOf: url))
+    }
+
+    func with(
+        accessor: NDAccessor? = nil,
+        category: String? = nil,
+        identifier: String? = nil,
+        kind: String? = nil,
+        timeStamp: String? = nil,
+        type: String? = nil
+    ) -> NDPrivacyAccess {
+        return NDPrivacyAccess(
+            accessor: accessor ?? self.accessor,
+            category: category ?? self.category,
+            identifier: identifier ?? self.identifier,
+            kind: kind ?? self.kind,
+            timeStamp: timeStamp ?? self.timeStamp,
+            type: type ?? self.type
+        )
+    }
+
+    func jsonData() throws -> Data {
+        return try newJSONEncoder().encode(self)
+    }
+
+    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
+        return String(data: try jsonData(), encoding: encoding)
+    }
+}
+
+//
+// Hashable or Equatable:
+// The compiler will not be able to synthesize the implementation of Hashable or Equatable
+// for types that require the use of JSONAny, nor will the implementation of Hashable be
+// synthesized for types that have collections (such as arrays or dictionaries).
+
+// MARK: - NDAccessor
+
+public struct NDAccessor: Codable, Hashable {
+    public let identifier: String
+    public let identifierType: String
+
+    enum CodingKeys: String, CodingKey {
+        case identifier
+        case identifierType
+    }
+
+    public init(identifier: String, identifierType: String) {
+        self.identifier = identifier
+        self.identifierType = identifierType
+    }
+}
+
+// MARK: NDAccessor convenience initializers and mutators
+
+public extension NDAccessor {
+    init(data: Data) throws {
+        self = try newJSONDecoder().decode(NDAccessor.self, from: data)
+    }
+
+    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
+        guard let data = json.data(using: encoding) else {
+            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
+        }
+        try self.init(data: data)
+    }
+
+    init(fromURL url: URL) throws {
+        try self.init(data: try Data(contentsOf: url))
+    }
+
+    func with(
+        identifier: String? = nil,
+        identifierType: String? = nil
+    ) -> NDAccessor {
+        return NDAccessor(
+            identifier: identifier ?? self.identifier,
+            identifierType: identifierType ?? self.identifierType
+        )
+    }
+
+    func jsonData() throws -> Data {
+        return try newJSONEncoder().encode(self)
+    }
+
+    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
+        return String(data: try jsonData(), encoding: encoding)
+    }
+}
+
+// MARK: - Helper functions for creating encoders and decoders
+
+func newJSONDecoder() -> JSONDecoder {
+    let decoder = JSONDecoder()
+    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
+        decoder.dateDecodingStrategy = .iso8601
+    }
+    return decoder
+}
+
+func newJSONEncoder() -> JSONEncoder {
+    let encoder = JSONEncoder()
+    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
+        encoder.dateEncodingStrategy = .iso8601
+    }
+    return encoder
+}
+
+// This file was generated from JSON Schema using quicktype, do not modify it directly.
+// To parse the JSON, add this file to your project and do:
+//
+//   let NDNetworkAccess = try NDNetworkAccess(json)
+
+//
+// Hashable or Equatable:
+// The compiler will not be able to synthesize the implementation of Hashable or Equatable
+// for types that require the use of JSONAny, nor will the implementation of Hashable be
+// synthesized for types that have collections (such as arrays or dictionaries).
+
+import Foundation
+
+// MARK: - NDNetworkAccess
+
+public struct NDNetworkAccess: Codable, Hashable {
+    public let domain: String
+    public let firstTimeStamp: String
+    public let context: String
+    public let timeStamp: String
+    public let domainType: Int
+    public let initiatedType: String
+    public let hits: Int
+    public let type: String
+    public let domainOwner: String
+    public let bundleid: String
+
+    enum CodingKeys: String, CodingKey {
+        case domain
+        case firstTimeStamp
+        case context
+        case timeStamp
+        case domainType
+        case initiatedType
+        case hits
+        case type
+        case domainOwner
+        case bundleid = "bundleID"
+    }
+
+    public init(domain: String, firstTimeStamp: String, context: String, timeStamp: String, domainType: Int, initiatedType: String, hits: Int, type: String, domainOwner: String, bundleid: String) {
+        self.domain = domain
+        self.firstTimeStamp = firstTimeStamp
+        self.context = context
+        self.timeStamp = timeStamp
+        self.domainType = domainType
+        self.initiatedType = initiatedType
+        self.hits = hits
+        self.type = type
+        self.domainOwner = domainOwner
+        self.bundleid = bundleid
+    }
+}
+
+// MARK: NDNetworkAccess convenience initializers and mutators
+
+public extension NDNetworkAccess {
+    init(data: Data) throws {
+        self = try newJSONDecoder().decode(NDNetworkAccess.self, from: data)
+    }
+
+    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
+        guard let data = json.data(using: encoding) else {
+            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
+        }
+        try self.init(data: data)
+    }
+
+    init(fromURL url: URL) throws {
+        try self.init(data: try Data(contentsOf: url))
+    }
+
+    func with(
+        domain: String? = nil,
+        firstTimeStamp: String? = nil,
+        context: String? = nil,
+        timeStamp: String? = nil,
+        domainType: Int? = nil,
+        initiatedType: String? = nil,
+        hits: Int? = nil,
+        type: String? = nil,
+        domainOwner: String? = nil,
+        bundleid: String? = nil
+    ) -> NDNetworkAccess {
+        return NDNetworkAccess(
+            domain: domain ?? self.domain,
+            firstTimeStamp: firstTimeStamp ?? self.firstTimeStamp,
+            context: context ?? self.context,
+            timeStamp: timeStamp ?? self.timeStamp,
+            domainType: domainType ?? self.domainType,
+            initiatedType: initiatedType ?? self.initiatedType,
+            hits: hits ?? self.hits,
+            type: type ?? self.type,
+            domainOwner: domainOwner ?? self.domainOwner,
+            bundleid: bundleid ?? self.bundleid
+        )
+    }
+
+    func jsonData() throws -> Data {
+        return try newJSONEncoder().encode(self)
+    }
+
+    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
+        return String(data: try jsonData(), encoding: encoding)
+    }
+}
+
+// This file was generated from JSON Schema using quicktype, do not modify it directly.
+// To parse the JSON, add this file to your project and do:
+//
+//   let ASAPIResult = try ASAPIResult(json)
+
+//
+// Hashable or Equatable:
+// The compiler will not be able to synthesize the implementation of Hashable or Equatable
+// for types that require the use of JSONAny, nor will the implementation of Hashable be
+// synthesized for types that have collections (such as arrays or dictionaries).
+
+// MARK: - ASAPIResult
+
+public struct ASAPIResult: Codable {
+    public let resultCount: Int?
+    public let results: [ASResult]?
+
+    enum CodingKeys: String, CodingKey {
+        case resultCount
+        case results
+    }
+
+    public init(resultCount: Int?, results: [ASResult]?) {
+        self.resultCount = resultCount
+        self.results = results
+    }
+}
+
+// MARK: ASAPIResult convenience initializers and mutators
+
+public extension ASAPIResult {
+    init(data: Data) throws {
+        self = try newJSONDecoder().decode(ASAPIResult.self, from: data)
+    }
+
+    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
+        guard let data = json.data(using: encoding) else {
+            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
+        }
+        try self.init(data: data)
+    }
+
+    init(fromURL url: URL) throws {
+        try self.init(data: try Data(contentsOf: url))
+    }
+
+    func with(
+        resultCount: Int?? = nil,
+        results: [ASResult]?? = nil
+    ) -> ASAPIResult {
+        return ASAPIResult(
+            resultCount: resultCount ?? self.resultCount,
+            results: results ?? self.results
+        )
+    }
+
+    func jsonData() throws -> Data {
+        return try newJSONEncoder().encode(self)
+    }
+
+    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
+        return String(data: try jsonData(), encoding: encoding)
+    }
+}
+
+//
+// Hashable or Equatable:
+// The compiler will not be able to synthesize the implementation of Hashable or Equatable
+// for types that require the use of JSONAny, nor will the implementation of Hashable be
+// synthesized for types that have collections (such as arrays or dictionaries).
+
+// MARK: - ASResult
+
+public struct ASResult: Codable {
+    public let screenshotUrls: [String]?
+    public let ipadScreenshotUrls: [String]?
+    public let appletvScreenshotUrls: [JSONAny]?
+    public let artworkUrl60: String?
+    public let artworkUrl512: String?
+    public let artworkUrl100: String?
+    public let artistViewurl: String?
+    public let features: [String]?
+    public let supportedDevices: [String]?
+    public let advisories: [String]?
+    public let isGameCenterEnabled: Bool?
+    public let kind: String?
+    public let averageUserRating: Double?
+    public let minimumosVersion: String?
+    public let trackCensoredName: String?
+    public let languageCodesiso2a: [String]?
+    public let fileSizeBytes: String?
+    public let sellerurl: String?
+    public let formattedPrice: String?
+    public let contentAdvisoryRating: String?
+    public let averageUserRatingForCurrentVersion: Double?
+    public let userRatingCountForCurrentVersion: Int?
+    public let trackViewurl: String?
+    public let trackContentRating: String?
+    public let bundleid: String?
+    public let trackid: Int?
+    public let trackName: String?
+    public let releaseDate: Date?
+    public let sellerName: String?
+    public let primaryGenreName: String?
+    public let genreids: [String]?
+    public let isVppDeviceBasedLicensingEnabled: Bool?
+    public let currentVersionReleaseDate: Date?
+    public let releaseNotes: String?
+    public let primaryGenreid: Int?
+    public let currency: String?
+    public let resultDescription: String?
+    public let artistid: Int?
+    public let artistName: String?
+    public let genres: [String]?
+    public let price: Int?
+    public let version: String?
+    public let wrapperType: String?
+    public let userRatingCount: Int?
+
+    enum CodingKeys: String, CodingKey {
+        case screenshotUrls
+        case ipadScreenshotUrls
+        case appletvScreenshotUrls
+        case artworkUrl60
+        case artworkUrl512
+        case artworkUrl100
+        case artistViewurl = "artistViewUrl"
+        case features
+        case supportedDevices
+        case advisories
+        case isGameCenterEnabled
+        case kind
+        case averageUserRating
+        case minimumosVersion = "minimumOsVersion"
+        case trackCensoredName
+        case languageCodesiso2a = "languageCodesISO2A"
+        case fileSizeBytes
+        case sellerurl = "sellerUrl"
+        case formattedPrice
+        case contentAdvisoryRating
+        case averageUserRatingForCurrentVersion
+        case userRatingCountForCurrentVersion
+        case trackViewurl = "trackViewUrl"
+        case trackContentRating
+        case bundleid = "bundleId"
+        case trackid = "trackId"
+        case trackName
+        case releaseDate
+        case sellerName
+        case primaryGenreName
+        case genreids = "genreIds"
+        case isVppDeviceBasedLicensingEnabled
+        case currentVersionReleaseDate
+        case releaseNotes
+        case primaryGenreid = "primaryGenreId"
+        case currency
+        case resultDescription = "description"
+        case artistid = "artistId"
+        case artistName
+        case genres
+        case price
+        case version
+        case wrapperType
+        case userRatingCount
+    }
+
+    public init(screenshotUrls: [String]?, ipadScreenshotUrls: [String]?, appletvScreenshotUrls: [JSONAny]?, artworkUrl60: String?, artworkUrl512: String?, artworkUrl100: String?, artistViewurl: String?, features: [String]?, supportedDevices: [String]?, advisories: [String]?, isGameCenterEnabled: Bool?, kind: String?, averageUserRating: Double?, minimumosVersion: String?, trackCensoredName: String?, languageCodesiso2a: [String]?, fileSizeBytes: String?, sellerurl: String?, formattedPrice: String?, contentAdvisoryRating: String?, averageUserRatingForCurrentVersion: Double?, userRatingCountForCurrentVersion: Int?, trackViewurl: String?, trackContentRating: String?, bundleid: String?, trackid: Int?, trackName: String?, releaseDate: Date?, sellerName: String?, primaryGenreName: String?, genreids: [String]?, isVppDeviceBasedLicensingEnabled: Bool?, currentVersionReleaseDate: Date?, releaseNotes: String?, primaryGenreid: Int?, currency: String?, resultDescription: String?, artistid: Int?, artistName: String?, genres: [String]?, price: Int?, version: String?, wrapperType: String?, userRatingCount: Int?) {
+        self.screenshotUrls = screenshotUrls
+        self.ipadScreenshotUrls = ipadScreenshotUrls
+        self.appletvScreenshotUrls = appletvScreenshotUrls
+        self.artworkUrl60 = artworkUrl60
+        self.artworkUrl512 = artworkUrl512
+        self.artworkUrl100 = artworkUrl100
+        self.artistViewurl = artistViewurl
+        self.features = features
+        self.supportedDevices = supportedDevices
+        self.advisories = advisories
+        self.isGameCenterEnabled = isGameCenterEnabled
+        self.kind = kind
+        self.averageUserRating = averageUserRating
+        self.minimumosVersion = minimumosVersion
+        self.trackCensoredName = trackCensoredName
+        self.languageCodesiso2a = languageCodesiso2a
+        self.fileSizeBytes = fileSizeBytes
+        self.sellerurl = sellerurl
+        self.formattedPrice = formattedPrice
+        self.contentAdvisoryRating = contentAdvisoryRating
+        self.averageUserRatingForCurrentVersion = averageUserRatingForCurrentVersion
+        self.userRatingCountForCurrentVersion = userRatingCountForCurrentVersion
+        self.trackViewurl = trackViewurl
+        self.trackContentRating = trackContentRating
+        self.bundleid = bundleid
+        self.trackid = trackid
+        self.trackName = trackName
+        self.releaseDate = releaseDate
+        self.sellerName = sellerName
+        self.primaryGenreName = primaryGenreName
+        self.genreids = genreids
+        self.isVppDeviceBasedLicensingEnabled = isVppDeviceBasedLicensingEnabled
+        self.currentVersionReleaseDate = currentVersionReleaseDate
+        self.releaseNotes = releaseNotes
+        self.primaryGenreid = primaryGenreid
+        self.currency = currency
+        self.resultDescription = resultDescription
+        self.artistid = artistid
+        self.artistName = artistName
+        self.genres = genres
+        self.price = price
+        self.version = version
+        self.wrapperType = wrapperType
+        self.userRatingCount = userRatingCount
+    }
+}
+
+// MARK: ASResult convenience initializers and mutators
+
+public extension ASResult {
+    init(data: Data) throws {
+        self = try newJSONDecoder().decode(ASResult.self, from: data)
+    }
+
+    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
+        guard let data = json.data(using: encoding) else {
+            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
+        }
+        try self.init(data: data)
+    }
+
+    init(fromURL url: URL) throws {
+        try self.init(data: try Data(contentsOf: url))
+    }
+
+    func with(
+        screenshotUrls: [String]?? = nil,
+        ipadScreenshotUrls: [String]?? = nil,
+        appletvScreenshotUrls: [JSONAny]?? = nil,
+        artworkUrl60: String?? = nil,
+        artworkUrl512: String?? = nil,
+        artworkUrl100: String?? = nil,
+        artistViewurl: String?? = nil,
+        features: [String]?? = nil,
+        supportedDevices: [String]?? = nil,
+        advisories: [String]?? = nil,
+        isGameCenterEnabled: Bool?? = nil,
+        kind: String?? = nil,
+        averageUserRating: Double?? = nil,
+        minimumosVersion: String?? = nil,
+        trackCensoredName: String?? = nil,
+        languageCodesiso2a: [String]?? = nil,
+        fileSizeBytes: String?? = nil,
+        sellerurl: String?? = nil,
+        formattedPrice: String?? = nil,
+        contentAdvisoryRating: String?? = nil,
+        averageUserRatingForCurrentVersion: Double?? = nil,
+        userRatingCountForCurrentVersion: Int?? = nil,
+        trackViewurl: String?? = nil,
+        trackContentRating: String?? = nil,
+        bundleid: String?? = nil,
+        trackid: Int?? = nil,
+        trackName: String?? = nil,
+        releaseDate: Date?? = nil,
+        sellerName: String?? = nil,
+        primaryGenreName: String?? = nil,
+        genreids: [String]?? = nil,
+        isVppDeviceBasedLicensingEnabled: Bool?? = nil,
+        currentVersionReleaseDate: Date?? = nil,
+        releaseNotes: String?? = nil,
+        primaryGenreid: Int?? = nil,
+        currency: String?? = nil,
+        resultDescription: String?? = nil,
+        artistid: Int?? = nil,
+        artistName: String?? = nil,
+        genres: [String]?? = nil,
+        price: Int?? = nil,
+        version: String?? = nil,
+        wrapperType: String?? = nil,
+        userRatingCount: Int?? = nil
+    ) -> ASResult {
+        return ASResult(
+            screenshotUrls: screenshotUrls ?? self.screenshotUrls,
+            ipadScreenshotUrls: ipadScreenshotUrls ?? self.ipadScreenshotUrls,
+            appletvScreenshotUrls: appletvScreenshotUrls ?? self.appletvScreenshotUrls,
+            artworkUrl60: artworkUrl60 ?? self.artworkUrl60,
+            artworkUrl512: artworkUrl512 ?? self.artworkUrl512,
+            artworkUrl100: artworkUrl100 ?? self.artworkUrl100,
+            artistViewurl: artistViewurl ?? self.artistViewurl,
+            features: features ?? self.features,
+            supportedDevices: supportedDevices ?? self.supportedDevices,
+            advisories: advisories ?? self.advisories,
+            isGameCenterEnabled: isGameCenterEnabled ?? self.isGameCenterEnabled,
+            kind: kind ?? self.kind,
+            averageUserRating: averageUserRating ?? self.averageUserRating,
+            minimumosVersion: minimumosVersion ?? self.minimumosVersion,
+            trackCensoredName: trackCensoredName ?? self.trackCensoredName,
+            languageCodesiso2a: languageCodesiso2a ?? self.languageCodesiso2a,
+            fileSizeBytes: fileSizeBytes ?? self.fileSizeBytes,
+            sellerurl: sellerurl ?? self.sellerurl,
+            formattedPrice: formattedPrice ?? self.formattedPrice,
+            contentAdvisoryRating: contentAdvisoryRating ?? self.contentAdvisoryRating,
+            averageUserRatingForCurrentVersion: averageUserRatingForCurrentVersion ?? self.averageUserRatingForCurrentVersion,
+            userRatingCountForCurrentVersion: userRatingCountForCurrentVersion ?? self.userRatingCountForCurrentVersion,
+            trackViewurl: trackViewurl ?? self.trackViewurl,
+            trackContentRating: trackContentRating ?? self.trackContentRating,
+            bundleid: bundleid ?? self.bundleid,
+            trackid: trackid ?? self.trackid,
+            trackName: trackName ?? self.trackName,
+            releaseDate: releaseDate ?? self.releaseDate,
+            sellerName: sellerName ?? self.sellerName,
+            primaryGenreName: primaryGenreName ?? self.primaryGenreName,
+            genreids: genreids ?? self.genreids,
+            isVppDeviceBasedLicensingEnabled: isVppDeviceBasedLicensingEnabled ?? self.isVppDeviceBasedLicensingEnabled,
+            currentVersionReleaseDate: currentVersionReleaseDate ?? self.currentVersionReleaseDate,
+            releaseNotes: releaseNotes ?? self.releaseNotes,
+            primaryGenreid: primaryGenreid ?? self.primaryGenreid,
+            currency: currency ?? self.currency,
+            resultDescription: resultDescription ?? self.resultDescription,
+            artistid: artistid ?? self.artistid,
+            artistName: artistName ?? self.artistName,
+            genres: genres ?? self.genres,
+            price: price ?? self.price,
+            version: version ?? self.version,
+            wrapperType: wrapperType ?? self.wrapperType,
+            userRatingCount: userRatingCount ?? self.userRatingCount
+        )
+    }
+
+    func jsonData() throws -> Data {
+        return try newJSONEncoder().encode(self)
+    }
+
+    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
+        return String(data: try jsonData(), encoding: encoding)
+    }
+}
+
+// MARK: - Encode/decode helpers
+
+public class JSONNull: Codable, Hashable {
+    public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
+        return true
+    }
+
+    public var hashValue: Int {
+        return 0
+    }
+
+    public func hash(into hasher: inout Hasher) {
+        // No-op
+    }
+
+    public init() {}
+
+    public required init(from decoder: Decoder) throws {
+        let container = try decoder.singleValueContainer()
+        if !container.decodeNil() {
+            throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
+        }
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.singleValueContainer()
+        try container.encodeNil()
+    }
+}
+
+class JSONCodingKey: CodingKey {
+    let key: String
+
+    required init?(intValue: Int) {
+        return nil
+    }
+
+    required init?(stringValue: String) {
+        key = stringValue
+    }
+
+    var intValue: Int? {
+        return nil
+    }
+
+    var stringValue: String {
+        return key
+    }
+}
+
+public class JSONAny: Codable {
+    public let value: Any
+
+    static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
+        let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
+        return DecodingError.typeMismatch(JSONAny.self, context)
+    }
+
+    static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError {
+        let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny")
+        return EncodingError.invalidValue(value, context)
+    }
+
+    static func decode(from container: SingleValueDecodingContainer) throws -> Any {
+        if let value = try? container.decode(Bool.self) {
+            return value
+        }
+        if let value = try? container.decode(Int64.self) {
+            return value
+        }
+        if let value = try? container.decode(Double.self) {
+            return value
+        }
+        if let value = try? container.decode(String.self) {
+            return value
+        }
+        if container.decodeNil() {
+            return JSONNull()
+        }
+        throw decodingError(forCodingPath: container.codingPath)
+    }
+
+    static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any {
+        if let value = try? container.decode(Bool.self) {
+            return value
+        }
+        if let value = try? container.decode(Int64.self) {
+            return value
+        }
+        if let value = try? container.decode(Double.self) {
+            return value
+        }
+        if let value = try? container.decode(String.self) {
+            return value
+        }
+        if let value = try? container.decodeNil() {
+            if value {
+                return JSONNull()
+            }
+        }
+        if var container = try? container.nestedUnkeyedContainer() {
+            return try decodeArray(from: &container)
+        }
+        if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) {
+            return try decodeDictionary(from: &container)
+        }
+        throw decodingError(forCodingPath: container.codingPath)
+    }
+
+    static func decode(from container: inout KeyedDecodingContainer<JSONCodingKey>, forKey key: JSONCodingKey) throws -> Any {
+        if let value = try? container.decode(Bool.self, forKey: key) {
+            return value
+        }
+        if let value = try? container.decode(Int64.self, forKey: key) {
+            return value
+        }
+        if let value = try? container.decode(Double.self, forKey: key) {
+            return value
+        }
+        if let value = try? container.decode(String.self, forKey: key) {
+            return value
+        }
+        if let value = try? container.decodeNil(forKey: key) {
+            if value {
+                return JSONNull()
+            }
+        }
+        if var container = try? container.nestedUnkeyedContainer(forKey: key) {
+            return try decodeArray(from: &container)
+        }
+        if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) {
+            return try decodeDictionary(from: &container)
+        }
+        throw decodingError(forCodingPath: container.codingPath)
+    }
+
+    static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] {
+        var arr: [Any] = []
+        while !container.isAtEnd {
+            let value = try decode(from: &container)
+            arr.append(value)
+        }
+        return arr
+    }
+
+    static func decodeDictionary(from container: inout KeyedDecodingContainer<JSONCodingKey>) throws -> [String: Any] {
+        var dict = [String: Any]()
+        for key in container.allKeys {
+            let value = try decode(from: &container, forKey: key)
+            dict[key.stringValue] = value
+        }
+        return dict
+    }
+
+    static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws {
+        for value in array {
+            if let value = value as? Bool {
+                try container.encode(value)
+            } else if let value = value as? Int64 {
+                try container.encode(value)
+            } else if let value = value as? Double {
+                try container.encode(value)
+            } else if let value = value as? String {
+                try container.encode(value)
+            } else if value is JSONNull {
+                try container.encodeNil()
+            } else if let value = value as? [Any] {
+                var container = container.nestedUnkeyedContainer()
+                try encode(to: &container, array: value)
+            } else if let value = value as? [String: Any] {
+                var container = container.nestedContainer(keyedBy: JSONCodingKey.self)
+                try encode(to: &container, dictionary: value)
+            } else {
+                throw encodingError(forValue: value, codingPath: container.codingPath)
+            }
+        }
+    }
+
+    static func encode(to container: inout KeyedEncodingContainer<JSONCodingKey>, dictionary: [String: Any]) throws {
+        for (key, value) in dictionary {
+            let key = JSONCodingKey(stringValue: key)!
+            if let value = value as? Bool {
+                try container.encode(value, forKey: key)
+            } else if let value = value as? Int64 {
+                try container.encode(value, forKey: key)
+            } else if let value = value as? Double {
+                try container.encode(value, forKey: key)
+            } else if let value = value as? String {
+                try container.encode(value, forKey: key)
+            } else if value is JSONNull {
+                try container.encodeNil(forKey: key)
+            } else if let value = value as? [Any] {
+                var container = container.nestedUnkeyedContainer(forKey: key)
+                try encode(to: &container, array: value)
+            } else if let value = value as? [String: Any] {
+                var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key)
+                try encode(to: &container, dictionary: value)
+            } else {
+                throw encodingError(forValue: value, codingPath: container.codingPath)
+            }
+        }
+    }
+
+    static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws {
+        if let value = value as? Bool {
+            try container.encode(value)
+        } else if let value = value as? Int64 {
+            try container.encode(value)
+        } else if let value = value as? Double {
+            try container.encode(value)
+        } else if let value = value as? String {
+            try container.encode(value)
+        } else if value is JSONNull {
+            try container.encodeNil()
+        } else {
+            throw encodingError(forValue: value, codingPath: container.codingPath)
+        }
+    }
+
+    public required init(from decoder: Decoder) throws {
+        if var arrayContainer = try? decoder.unkeyedContainer() {
+            value = try JSONAny.decodeArray(from: &arrayContainer)
+        } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) {
+            value = try JSONAny.decodeDictionary(from: &container)
+        } else {
+            let container = try decoder.singleValueContainer()
+            value = try JSONAny.decode(from: container)
+        }
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        if let arr = value as? [Any] {
+            var container = encoder.unkeyedContainer()
+            try JSONAny.encode(to: &container, array: arr)
+        } else if let dict = value as? [String: Any] {
+            var container = encoder.container(keyedBy: JSONCodingKey.self)
+            try JSONAny.encode(to: &container, dictionary: dict)
+        } else {
+            var container = encoder.singleValueContainer()
+            try JSONAny.encode(to: &container, value: value)
+        }
+    }
+}
diff --git a/insight/Preview Content/Preview Assets.xcassets/Contents.json b/insight/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/insight/Preview Content/Preview Assets.xcassets/Contents.json	
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/insight/insight.entitlements b/insight/insight.entitlements
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/insight/insight.entitlements
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict/>
+</plist>
diff --git a/insight/insightApp.swift b/insight/insightApp.swift
new file mode 100644
index 0000000..88502e6
--- /dev/null
+++ b/insight/insightApp.swift
@@ -0,0 +1,22 @@
+//
+//  insightApp.swift
+//  insight
+//
+//  Created by Lakr Aream on 2021/9/27.
+//
+
+import SwiftUI
+
+public var appStoreQueryCache: [URL: String] = [:]
+
+@main
+struct insightApp: App {
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+                .frame(minWidth: 800, idealWidth: 800, maxWidth: 50000,
+                       minHeight: 400, idealHeight: 600, maxHeight: 50000)
+        }
+        .windowStyle(HiddenTitleBarWindowStyle())
+    }
+}
diff --git a/insight/insightMain.swift b/insight/insightMain.swift
new file mode 100644
index 0000000..799113b
--- /dev/null
+++ b/insight/insightMain.swift
@@ -0,0 +1,168 @@
+//
+//  ContentView.swift
+//  insight
+//
+//  Created by Lakr Aream on 2021/9/27.
+//
+
+import Colorful
+import SwiftUI
+
+private let kDefaultBackgroundColors = [#colorLiteral(red: 0.9586862922, green: 0.660125792, blue: 0.8447988033, alpha: 1), #colorLiteral(red: 0.8714533448, green: 0.723166883, blue: 0.9342088699, alpha: 1), #colorLiteral(red: 0.7458761334, green: 0.7851135731, blue: 0.9899476171, alpha: 1)]
+    .map { Color($0) }
+
+struct ContentView: View {
+    @State var loading: Bool = false
+    @State var progress: Progress = Progress()
+    @State var insightReader: InsightReaderView?
+    @State var dragOver = false
+
+    var body: some View {
+        GeometryReader { reader in
+            if loading {
+                ZStack {
+                    VStack(spacing: 8) {
+                        ProgressView()
+                        ProgressView(progress)
+                            .progressViewStyle(LinearProgressViewStyle())
+                            .animation(.interactiveSpring(), value: progress)
+                            .padding()
+                        Text("Building your summary, please wait... ☕️")
+                            .font(.system(size: 12, weight: .semibold, design: .rounded))
+                    }
+                    .frame(width: 300)
+                }
+                .frame(width: reader.size.width, height: reader.size.height)
+            } else if let insightReader = insightReader {
+                insightReader
+            } else {
+                ZStack {
+                    ColorfulView(colors: kDefaultBackgroundColors, colorCount: 16)
+                    VStack {
+                        Image("RoundedIcon")
+                            .resizable()
+                            .frame(width: 80, height: 80)
+                        Text("Drag your file here to analyze ~")
+                            .font(.system(size: 16, weight: .semibold, design: .rounded))
+                    }
+                }
+                .frame(width: reader.size.width, height: reader.size.height)
+                .overlay(
+                    ZStack {
+                        Button {
+                            let panel = NSOpenPanel()
+                            panel.allowsMultipleSelection = false
+                            panel.canChooseDirectories = false
+                            if panel.runModal() == .OK, let url = panel.url {
+                                loading = true
+                                DispatchQueue.global().async {
+                                    prepareInsightData(with: url)
+                                    DispatchQueue.main.async {
+                                        loading = false
+                                    }
+                                }
+                            }
+                        } label: {
+                            Image(systemName: "square.and.arrow.down.fill")
+                                .padding(2)
+                        }
+                        .offset(x: 35 - reader.size.width / 2, y: reader.size.height / 2 - 25)
+                    }
+                    .frame(width: reader.size.width, height: reader.size.height)
+                )
+            }
+        }
+        .onDrop(of: ["public.file-url"], isTargeted: $dragOver, perform: { providers in
+            providers
+                .first?
+                .loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { data, _ in
+                    if let data = data,
+                       let path = NSString(data: data, encoding: 4),
+                       let url = URL(string: path as String) {
+                        loading = true
+                        DispatchQueue.global().async {
+                            prepareInsightData(with: url)
+                            DispatchQueue.main.async {
+                                loading = false
+                            }
+                        }
+                    }
+                })
+            return true
+        })
+        .animation(.interactiveSpring(), value: loading)
+        .background(Color(NSColor.textBackgroundColor))
+        .ignoresSafeArea()
+    }
+
+    func prepareInsightData(with url: URL) {
+        debugPrint("[i] loading insight data \(url.path)")
+        guard url.pathExtension.lowercased() == "ndjson" else {
+            errorProcessingInsight(with: "Wrong format, requires .ndjson file.")
+            return
+        }
+        var read: Data?
+        do {
+            read = try Data(contentsOf: url)
+        } catch {
+            errorProcessingInsight(with: error.localizedDescription)
+        }
+        guard let read = read,
+              let text = String(data: read, encoding: .utf8)
+        else {
+            errorProcessingInsight(with: "Failed to decode record file.")
+            return
+        }
+        debugPrint("[i] loaded ndjson with length: \(read.count)")
+        var privacyAccessBuilder: [NDPrivacyAccess] = []
+        var networkAccessBuilder: [NDNetworkAccess] = []
+        analyzer: for line in text.components(separatedBy: "\n") {
+            let cleanedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
+            if cleanedLine.count < 1 { continue analyzer }
+            // try for each decoder
+            if let privacyAccess = try? NDPrivacyAccess(cleanedLine) {
+                privacyAccessBuilder.append(privacyAccess)
+                continue analyzer
+            }
+            if let networkAccess = try? NDNetworkAccess(cleanedLine) {
+                networkAccessBuilder.append(networkAccess)
+                continue analyzer
+            }
+            debugPrint("[E] ignoring unknown line")
+        }
+        let summary = NDPrivacySummary(privacyAccess: privacyAccessBuilder,
+                                       networkAccess: networkAccessBuilder) { pass in
+            updateProgress(total: pass.0, current: pass.1)
+        }
+        print(
+            """
+            Loaded privacy summary with \(summary.applicationSummary.count) applications
+            ===> Privacy Record \(summary.privacyAccess.count)
+            ===> Network Record \(summary.networkAccess.count)
+            """
+        )
+        if summary.privacyAccess.count < 1 && summary.networkAccess.count < 1 {
+            errorProcessingInsight(with: "Nothing to load.")
+            return
+        }
+        DispatchQueue.main.async {
+            insightReader = InsightReaderView(insightReport: summary)
+        }
+    }
+
+    func errorProcessingInsight(with reason: String) {
+        DispatchQueue.main.async {
+            let alert = NSAlert()
+            alert.messageText = reason
+            alert.runModal()
+        }
+    }
+    
+    func updateProgress(total: Int, current: Int) {
+        DispatchQueue.main.async {
+            let builder = Progress(totalUnitCount: Int64(exactly: total) ?? 0)
+            builder.completedUnitCount = Int64(exactly: current) ?? 0
+            progress = builder
+        }
+    }
+}