├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── Share2ArchiveToday
├── .gitignore
├── app
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── assets
│ │ └── data.minify.json
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ └── org
│ │ │ └── gnosco
│ │ │ └── share2archivetoday
│ │ │ ├── ClearUrlsRulesManager.kt
│ │ │ ├── Legacy.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── WebURLMatcher.kt
│ │ └── res
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── play_store_512.png
│ │ ├── values-af
│ │ └── strings.xml
│ │ ├── values-am
│ │ └── strings.xml
│ │ ├── values-ar
│ │ └── strings.xml
│ │ ├── values-az-rAZ
│ │ └── strings.xml
│ │ ├── values-be
│ │ └── strings.xml
│ │ ├── values-bg
│ │ └── strings.xml
│ │ ├── values-bn-rBD
│ │ └── strings.xml
│ │ ├── values-ca
│ │ └── strings.xml
│ │ ├── values-cs-rCZ
│ │ └── strings.xml
│ │ ├── values-da-rDK
│ │ └── strings.xml
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values-el-rGR
│ │ └── strings.xml
│ │ ├── values-et
│ │ └── strings.xml
│ │ ├── values-eu-rES
│ │ └── strings.xml
│ │ ├── values-fi-rFI
│ │ └── strings.xml
│ │ ├── values-fil
│ │ └── strings.xml
│ │ ├── values-fr-rCA
│ │ └── strings.xml
│ │ ├── values-fr-rFR
│ │ └── strings.xml
│ │ ├── values-gl-rES
│ │ └── strings.xml
│ │ ├── values-gu
│ │ └── strings.xml
│ │ ├── values-hi
│ │ └── strings.xml
│ │ ├── values-hr
│ │ └── strings.xml
│ │ ├── values-hy-rAM
│ │ └── strings.xml
│ │ ├── values-iw
│ │ └── strings.xml
│ │ ├── values-ka
│ │ └── strings.xml
│ │ ├── values-my-rMM
│ │ └── strings.xml
│ │ ├── values-nl-rNL
│ │ └── strings.xml
│ │ ├── values-sq
│ │ └── strings.xml
│ │ ├── values-zh-rCN
│ │ └── strings.xml
│ │ ├── values-zh-rHK
│ │ └── strings.xml
│ │ ├── values-zh-rTW
│ │ └── strings.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
├── fastlane
└── metadata
│ └── android
│ ├── ar
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ └── en-US
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 0.png
│ │ ├── 1.png
│ │ ├── 10.png
│ │ ├── 11.png
│ │ ├── 12.png
│ │ ├── 13.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ ├── 6.png
│ │ ├── 7.png
│ │ ├── 8.png
│ │ └── 9.png
│ ├── short_description.txt
│ └── title.txt
├── ios
└── Share-2-Archive-Today
│ ├── Share-2-Archive-Today.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ ├── Share-2-Archive-Today.xcscheme
│ │ │ └── URLShareExtension.xcscheme
│ └── xcuserdata
│ │ └── gabirelfair.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ ├── Share-2-Archive-Today
│ ├── AppDelegate.swift
│ ├── ArchiveURLService.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ └── icon.png
│ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── FAQController.swift
│ ├── FloatingButton.swift
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ ├── Share-2-Archive-Today.entitlements
│ ├── URLProcessor.swift
│ ├── URLStore.swift
│ ├── ViewController.swift
│ └── WelcomeOverlayView.swift
│ └── URLShareExtension
│ ├── Base.lproj
│ └── MainInterface.storyboard
│ ├── Info.plist
│ ├── PrivacyInfo.xcprivacy
│ ├── QRCodeScanner.swift
│ ├── ShareViewController.swift
│ ├── Toast.swift
│ ├── URLProcessingManager.swift
│ └── URLShareExtension.entitlements
└── privacy.policy
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | captures/
13 | .externalNativeBuild/
14 | .cxx/
15 | *.apk
16 | output.json
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 | misc.xml
22 | deploymentTargetDropDown.xml
23 | render.experimental.xml
24 |
25 | # Keystore files
26 | *.jks
27 | *.keystore
28 |
29 | # Google Services (e.g. APIs or Firebase)
30 | google-services.json
31 |
32 | # Android Profiling
33 | *.hprof
34 | .DS_Store
35 |
36 |
37 | *.iml
38 | .gradle
39 | /local.properties
40 | /.idea/caches
41 | /.idea/libraries
42 | /.idea/modules.xml
43 | /.idea/workspace.xml
44 | /.idea/navEditor.xml
45 | /.idea/assetWizardSettings.xml
46 | .DS_Store
47 | /build
48 | /captures
49 | .externalNativeBuild
50 | .cxx
51 | local.properties
52 | key2
53 | key3
54 | /Share2ArchiveToday/app/release
55 | *.kts
56 | *.dm
57 | *.der
58 | *.pem
59 | ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.xcworkspace/xcuserdata/gabirelfair.xcuserdatad/UserInterfaceState.xcuserstate
60 | /ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.xcworkspace
61 | /ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.xcworkspace/xcuserdata
62 | ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.xcworkspace/xcuserdata/gabirelfair.xcuserdatad/UserInterfaceState.xcuserstate
63 | ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.xcworkspace/xcuserdata/gabirelfair.xcuserdatad/UserInterfaceState.xcuserstate
64 | Share2ArchiveToday/app/mapping.txt
65 | key9
66 | Share2ArchiveToday/app/seeds.txt
67 | Share2ArchiveToday/app/unused.txt
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Share 2 Archive Today
2 | [
](https://apps.apple.com/us/app/share-2-archive-today/id6742428176)
3 | [](https://chromewebstore.google.com/detail/archive-webpage/falfcajjjjfjjlfabnfaadepcoagegip)
4 |
5 | [
](https://play.google.com/store/apps/details?id=org.gnosco.share2archivetoday)
6 | [
](https://f-droid.org/en/packages/org.gnosco.share2archivetoday/)
9 | [
](https://apt.izzysoft.de/fdroid/index/apk/org.gnosco.share2archivetoday?repo=main)
12 |
13 |
14 |
15 |
16 |
17 | Simple app to add an icon to your share menu to publically archive a URL on Archive.today and Archive.Is
18 |
19 | It does not spy on you or collect any data. Find it on the [Google Play Store](https://play.google.com/store/apps/details?id=org.gnosco.share2archivetoday).
20 |
21 | Find the companion chrome browser app on the [Chrome Web Store](https://chromewebstore.google.com/detail/archive-webpage/falfcajjjjfjjlfabnfaadepcoagegip).
22 |
23 | ## _Features_
24 | - ⭐️ Finds the first link in any shared text, no need to select just the url
25 | - ⭐️ Protect your privacy by removing tracking tokens in URL
26 | - ⭐️ Substack email nag screens are now bypassed
27 | - ⭐️ Outlinks can be archived directly from archived pages
28 | - ⭐️ WesternJournal.com URLs can now be archived
29 |
30 |
31 |
32 |
33 | Also, in order for this app to be listed on the google play store, I have to reference the [privacy polcy](https://github.com/gabefair/Share-2-Archive-Today/blob/main/privacy.policy).
34 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.jetbrains.kotlin.android)
4 | }
5 |
6 | android {
7 | namespace = "org.gnosco.share2archivetoday"
8 | compileSdk = 35
9 |
10 | defaultConfig {
11 | applicationId = "org.gnosco.share2archivetoday"
12 | minSdk = 3
13 | targetSdk = 34
14 | versionCode = 44
15 | versionName = "4.4"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | isMinifyEnabled = true
21 | isShrinkResources = true
22 | proguardFiles(
23 | getDefaultProguardFile("proguard-android-optimize.txt"),
24 | "proguard-rules.pro"
25 | )
26 | }
27 | debug {
28 | isTestCoverageEnabled = true
29 | }
30 | }
31 |
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_1_8
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = "1.8"
39 | }
40 |
41 | packaging {
42 | resources {
43 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
44 | }
45 | }
46 |
47 | dependenciesInfo { // The name of these variables are misleading, they need to be false in order to make the app more transparent.
48 | includeInApk = false
49 | includeInBundle = false
50 | }
51 | }
52 |
53 | dependencies {
54 | implementation("com.google.zxing:core:3.5.3")
55 | }
56 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | ##---------------Begin: App Specific Rules ----------
2 | -keep class org.gnosco.share2archivetoday.MainActivity {*;}
3 | -keep class androidx.compose.** { *; }
4 |
5 | ##---------------Begin: Stripping Logs ----------
6 | # Strip all log calls
7 | -assumenosideeffects class android.util.Log {
8 | public static boolean isLoggable(java.lang.String, int);
9 | public static int v(...);
10 | public static int d(...);
11 | public static int i(...);
12 | public static int w(...);
13 | public static int e(...);
14 | public static int wtf(...);
15 | }
16 |
17 | ##---------------Begin: ZXing Library Rules ----------
18 | # Keep only necessary ZXing classes
19 | -keep class com.google.zxing.BarcodeFormat { *; }
20 | -keep class com.google.zxing.DecodeHintType { *; }
21 | -keep class com.google.zxing.MultiFormatReader { *; }
22 | -keep class com.google.zxing.Result { *; }
23 | -keep class com.google.zxing.BinaryBitmap { *; }
24 | -keep class com.google.zxing.RGBLuminanceSource { *; }
25 | -keep class com.google.zxing.common.HybridBinarizer { *; }
26 | -keep class com.google.zxing.NotFoundException { *; }
27 |
28 | ##---------------Begin: General Optimization Rules ----------
29 | # Remove all debugging info from all classes
30 | -optimizationpasses 5
31 | -dontusemixedcaseclassnames
32 | -dontskipnonpubliclibraryclasses
33 | -dontskipnonpubliclibraryclassmembers
34 | -dontpreverify
35 | -verbose
36 | -dump class_files.txt
37 | -printseeds seeds.txt
38 | -printusage unused.txt
39 | -printmapping mapping.txt
40 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
41 |
42 | # Preserve the special static methods that are required in all enumeration classes
43 | -keepclassmembers enum * {
44 | public static **[] values();
45 | public static ** valueOf(java.lang.String);
46 | }
47 |
48 | # Keep necessary Android components
49 | -keep public class * extends android.app.Activity
50 | -keep public class * extends android.app.Application
51 | -keep public class * extends android.content.BroadcastReceiver
52 | -keep public class * extends android.content.ContentProvider
53 |
54 | # Keep legacy extension methods
55 | -keepclassmembers class org.gnosco.share2archivetoday.* {
56 | public static * legacy*(...);
57 | }
58 |
59 | # Keep the application's entry points
60 | -keepattributes *Annotation*
61 |
62 | # Remove unused code, resources, attributes in XMLs
63 | -keepattributes SourceFile,LineNumberTable
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/java/org/gnosco/share2archivetoday/Legacy.kt:
--------------------------------------------------------------------------------
1 | package org.gnosco.share2archivetoday
2 |
3 | import android.net.Uri
4 | import java.util.Collections
5 |
6 | fun Uri.legacyGetQueryParameterNames(): Set {
7 | if (isOpaque()) {
8 | throw UnsupportedOperationException("This isn't a hierarchical URI.")
9 | }
10 |
11 | val query: String = getEncodedQuery()
12 | ?: return emptySet()
13 |
14 | val names: MutableSet = LinkedHashSet()
15 | var start = 0
16 | do {
17 | val next = query.indexOf('&', start)
18 | val end = if ((next == -1)) query.length else next
19 |
20 | var separator = query.indexOf('=', start)
21 | if (separator > end || separator == -1) {
22 | separator = end
23 | }
24 |
25 | val name = query.substring(start, separator)
26 | names.add(Uri.decode(name))
27 |
28 | // Move start to end of name.
29 | start = end + 1
30 | } while (start < query.length)
31 |
32 | return Collections.unmodifiableSet(names)
33 | }
34 |
35 | fun Uri.Builder.legacyClearQuery(): Uri.Builder {
36 | return query(null)
37 | }
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/java/org/gnosco/share2archivetoday/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package org.gnosco.share2archivetoday
2 | // This file is: MainActivity.kt
3 |
4 | import WebURLMatcher
5 | import android.app.Activity
6 | import android.content.Intent
7 | import android.net.Uri
8 | import android.os.Bundle
9 | import android.util.Log
10 |
11 | import android.graphics.Bitmap
12 | import android.graphics.BitmapFactory
13 | import com.google.zxing.*
14 | import com.google.zxing.common.HybridBinarizer
15 | import kotlin.math.max
16 | import android.widget.Toast
17 |
18 | open class MainActivity : Activity() {
19 | private lateinit var clearUrlsRulesManager: ClearUrlsRulesManager
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 |
24 | // Initialize ClearURLs rules manager
25 | clearUrlsRulesManager = ClearUrlsRulesManager(applicationContext)
26 |
27 | handleShareIntent(intent)
28 | }
29 |
30 | override fun onNewIntent(intent: Intent?) {
31 | super.onNewIntent(intent)
32 | handleShareIntent(intent)
33 | }
34 |
35 | private fun handleShareIntent(intent: Intent?) {
36 | if (intent?.action == Intent.ACTION_SEND) {
37 | when (intent.type) {
38 | "text/plain" -> {
39 | intent.getStringExtra(Intent.EXTRA_TEXT)?.let { sharedText ->
40 | Log.d("MainActivity", "Shared text: $sharedText")
41 | val url = extractUrl(sharedText)
42 |
43 | if (url != null) {
44 | threeSteps(url)
45 | } else {
46 | Toast.makeText(this, "No URL found in shared text", Toast.LENGTH_SHORT).show()
47 | finish()
48 | }
49 | }
50 | }
51 | else -> {
52 | // Handle image shares
53 | if (intent.type?.startsWith("image/") == true) {
54 | try {
55 | val imageUri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
56 | intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
57 | } else {
58 | @Suppress("DEPRECATION")
59 | intent.getParcelableExtra(Intent.EXTRA_STREAM)
60 | }
61 |
62 | imageUri?.let {
63 | handleImageShare(it)
64 | }
65 | } catch (e: Exception) {
66 | Log.e("MainActivity", "Error handling image share", e)
67 | Toast.makeText(this, "Share 2 Archive did not like that image", Toast.LENGTH_SHORT).show()
68 | finish()
69 | }
70 | }
71 | }
72 | }
73 | }
74 | finish()
75 | }
76 |
77 | internal fun threeSteps(url: String) {
78 | val processedUrl = processArchiveUrl(url)
79 | val cleanedUrl = handleURL(processedUrl)
80 | openInBrowser("https://archive.today/?run=1&url=${Uri.encode(cleanedUrl)}")
81 | }
82 |
83 | internal fun handleImageShare(imageUri: Uri) {
84 | try {
85 | val qrUrl = extractUrl(extractQRCodeFromImage(imageUri))
86 | if (qrUrl != null) {
87 | threeSteps(qrUrl)
88 | Toast.makeText(this, "URL found in QR code", Toast.LENGTH_SHORT).show()
89 | } else {
90 | Log.d("MainActivity", "No QR code found in image")
91 | Toast.makeText(this, "No URL found in QR code image", Toast.LENGTH_SHORT).show()
92 | finish()
93 | }
94 | } catch (e: Exception) {
95 | Log.e("MainActivity", "Error processing QR code", e)
96 | Toast.makeText(this, "Error processing QR code", Toast.LENGTH_SHORT).show()
97 | finish()
98 | }
99 | }
100 |
101 | /**
102 | * Main URL handling method that combines ClearURLs rules with platform-specific optimizations
103 | */
104 | internal fun handleURL(url: String): String {
105 | // First clean with ClearURLs rules
106 | var rulesCleanedUrl = url
107 | if (clearUrlsRulesManager.areRulesLoaded()) {
108 | rulesCleanedUrl = clearUrlsRulesManager.clearUrl(url)
109 | }
110 | rulesCleanedUrl = cleanTrackingParamsFromUrl(rulesCleanedUrl)
111 |
112 | // Then apply additional platform-specific optimizations that might not be in the rules
113 | return applyPlatformSpecificOptimizations(rulesCleanedUrl)
114 | }
115 |
116 | /**
117 | * Apply platform-specific optimizations that may not be covered by ClearURLs rules
118 | */
119 | internal fun applyPlatformSpecificOptimizations(url: String): String {
120 | val uri = Uri.parse(url)
121 | val newUriBuilder = uri.buildUpon()
122 | var changed = false
123 |
124 | // YouTube-specific handling
125 | if (uri.host?.contains("youtube.com") == true || uri.host?.contains("youtu.be") == true) {
126 | // Convert shorts to regular videos
127 | if (uri.path?.contains("/shorts/") == true) {
128 | newUriBuilder.path(uri.path?.replace("/shorts/", "/v/"))
129 | changed = true
130 | }
131 |
132 | // Remove music. prefix
133 | val modifiedHost = uri.host?.removePrefix("music.")
134 | if (modifiedHost != uri.host) {
135 | newUriBuilder.authority(modifiedHost)
136 | changed = true
137 | }
138 |
139 | // Handle nested query parameters in YouTube search links
140 | val nestedQueryParams = uri.getQueryParameter("q")
141 | if (nestedQueryParams != null && nestedQueryParams.contains("?")) {
142 | try {
143 | val nestedUri = Uri.parse(nestedQueryParams)
144 | val newNestedUriBuilder = nestedUri.buildUpon().legacyClearQuery()
145 |
146 | nestedUri.legacyGetQueryParameterNames().forEach { nestedParam ->
147 | if (!isTrackingParam(nestedParam)) {
148 | newNestedUriBuilder.appendQueryParameter(nestedParam, nestedUri.getQueryParameter(nestedParam))
149 | }
150 | }
151 |
152 | newUriBuilder.appendQueryParameter("q", newNestedUriBuilder.build().toString())
153 | changed = true
154 | } catch (e: Exception) {
155 | Log.e("MainActivity", "Error handling nested query params", e)
156 | }
157 | }
158 | }
159 |
160 | // Substack-specific handling
161 | else if(uri.host?.endsWith(".substack.com") == true) {
162 | // Add "no_cover=true" parameter for better archive quality
163 | if (uri.getQueryParameter("no_cover") == null) {
164 | newUriBuilder.appendQueryParameter("no_cover", "true")
165 | changed = true
166 | }
167 | }
168 |
169 | else if (uri.host?.equals("t.me", ignoreCase = true) == true) {
170 | val path = uri.path?.trimStart('/') ?: ""
171 | if (!path.startsWith("s/") && path.isNotEmpty()) {
172 | newUriBuilder.path("/s/$path") //This is to archive some parts of the group chat if the web preview feature is enabled, otherwise the about page will be shown by telegram.
173 | changed = true
174 | }
175 | }
176 |
177 | return if (changed) newUriBuilder.build().toString() else url
178 | }
179 |
180 | internal fun extractQRCodeFromImage(imageUri: Uri): String {
181 | val inputStream = contentResolver.openInputStream(imageUri) ?: return ""
182 |
183 | // Read image dimensions first
184 | val options = BitmapFactory.Options().apply {
185 | inJustDecodeBounds = true
186 | }
187 | BitmapFactory.decodeStream(inputStream, null, options)
188 | inputStream.close()
189 |
190 | // Calculate sample size to avoid OOM
191 | val maxDimension = max(options.outWidth, options.outHeight)
192 | val sampleSize = max(1, maxDimension / 2048)
193 |
194 | // Read the actual bitmap with sampling
195 | val scaledOptions = BitmapFactory.Options().apply {
196 | inSampleSize = sampleSize
197 | inPreferredConfig = Bitmap.Config.ARGB_8888
198 | }
199 |
200 | contentResolver.openInputStream(imageUri)?.use { stream ->
201 | val bitmap = BitmapFactory.decodeStream(stream, null, scaledOptions) ?: return ""
202 |
203 | try {
204 | // Convert to ZXing format
205 | val width = bitmap.width
206 | val height = bitmap.height
207 | val pixels = IntArray(width * height)
208 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
209 |
210 | val source = RGBLuminanceSource(width, height, pixels)
211 | val binarizer = HybridBinarizer(source)
212 | val binaryBitmap = BinaryBitmap(binarizer)
213 |
214 | // Try to decode QR code
215 | val reader = MultiFormatReader()
216 | val hints = mapOf(
217 | DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE),
218 | DecodeHintType.TRY_HARDER to true
219 | )
220 |
221 | try {
222 | val result = reader.decode(binaryBitmap, hints)
223 | return result.text
224 | } catch (e: NotFoundException) {
225 | // No QR code found
226 | Log.d("MainActivity", "No QR code found in image")
227 | return ""
228 | }
229 | } finally {
230 | bitmap.recycle()
231 | }
232 | }
233 | return ""
234 | }
235 |
236 | internal fun processArchiveUrl(url: String): String {
237 | val uri = Uri.parse(url)
238 | val pattern = Regex("archive\\.[a-z]+/o/[a-zA-Z0-9]+/(.+)")
239 | val matchResult = pattern.find(uri.toString())
240 |
241 | return if (matchResult != null) {
242 | matchResult.groupValues[1]
243 | } else {
244 | url
245 | }
246 | }
247 |
248 | // Keep for fallback purposes
249 | private fun isTrackingParam(param: String): Boolean {
250 | val trackingParams = setOf(
251 | "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term",
252 | "fbclid", "gclid", "dclid", "gbraid", "wbraid", "msclkid", "tclid",
253 | "aff_id", "affiliate_id", "ref", "referer", "campaign_id", "ad_id",
254 | "adgroup_id", "adset_id", "creativetype", "placement", "network",
255 | "mc_eid", "mc_cid", "si", "icid", "_ga", "_gid", "scid", "click_id",
256 | "trk", "track", "trk_sid", "sid", "mibextid", "fb_action_ids",
257 | "fb_action_types", "twclid", "igshid", "s_kwcid", "sxsrf", "sca_esv",
258 | "source", "tbo", "sa", "ved", "pi", "fbs", "fbc", "fb_ref", "client", "ei",
259 | "gs_lp", "sclient", "oq", "uact", "bih", "biw", // sxsrf might be needed on some sites, but google uses it for tracking
260 | "m_entstream_source", "entstream_source", "fb_source",
261 | "ref_source", "ref_medium", "ref_campaign", "ref_content", "ref_term", "ref_keyword",
262 | "ref_type", "ref_campaign_id", "ref_ad_id", "ref_adgroup_id", "ref_adset_id",
263 | "ref_creativetype", "ref_placement", "ref_network", "ref_sid", "ref_mc_eid",
264 | "ref_mc_cid", "ref_scid", "ref_click_id", "ref_trk", "ref_track", "ref_trk_sid",
265 | "ref_sid", "ref", "ref_url", "ref_campaign_id", "ref_ad_id", "ref_adgroup_id", "ref_adset_id"
266 | )
267 | return param in trackingParams
268 | }
269 |
270 | internal fun isUnwantedYoutubeParam(param: String): Boolean {
271 | val youtubeParams = setOf(
272 | "feature",
273 | "ab_channel",
274 | "t",
275 | "si"
276 | )
277 | return param in youtubeParams
278 | }
279 | internal fun isUnwantedSubstackParam(param: String): Boolean {
280 | val substackParams = setOf(
281 | "r",
282 | "showWelcomeOnShare"
283 | )
284 | return param in substackParams
285 | }
286 |
287 | // Keep for fallback and special handling
288 | internal fun cleanTrackingParamsFromUrl(url: String): String {
289 | val uri = Uri.parse(url)
290 | if (uri.legacyGetQueryParameterNames().isEmpty()) {
291 | return url
292 | }
293 |
294 | val newUriBuilder = uri.buildUpon().legacyClearQuery()
295 | var removeYouTubeParams = false
296 | var removeSubstackParams = false
297 |
298 | // Additional handling for YouTube URLs
299 | if (uri.host?.contains("youtube.com") == true || uri.host?.contains("youtu.be") == true) {
300 | removeYouTubeParams = true
301 | val nestedQueryParams = uri.getQueryParameter("q")
302 | if (nestedQueryParams != null) {
303 | val nestedUri = Uri.parse(nestedQueryParams)
304 | val newNestedUriBuilder = nestedUri.buildUpon().legacyClearQuery()
305 |
306 | nestedUri.legacyGetQueryParameterNames().forEach { nestedParam ->
307 | if (!isTrackingParam(nestedParam)) {
308 | newNestedUriBuilder.appendQueryParameter(nestedParam, nestedUri.getQueryParameter(nestedParam))
309 | }
310 | }
311 | newUriBuilder.appendQueryParameter("q", newNestedUriBuilder.build().toString())
312 | }
313 | }
314 |
315 | else if(uri.host?.endsWith(".substack.com") == true) {
316 | removeSubstackParams = true
317 | }
318 |
319 | uri.legacyGetQueryParameterNames().forEach { param ->
320 | // Add only non-tracking parameters to the new URL
321 | if (!isTrackingParam(param) && !(removeYouTubeParams && isUnwantedYoutubeParam(param)) && !(removeSubstackParams && isUnwantedSubstackParam(param))) {
322 | newUriBuilder.appendQueryParameter(param, uri.getQueryParameter(param))
323 | }
324 | }
325 | return newUriBuilder.build().toString()
326 | }
327 |
328 | internal fun extractUrl(text: String): String? {
329 | // First try to find URLs with protocols
330 | val protocolMatcher = WebURLMatcher.matcher(text)
331 | if (protocolMatcher.find()) {
332 | return cleanUrl(protocolMatcher.group(0))
333 | }
334 |
335 | // If no URL with protocol is found, look for potential bare domains
336 | val domainPattern = Regex(
337 | "(?:^|\\s+)(" + // Start of string or whitespace
338 | "(?:[a-zA-Z0-9][a-zA-Z0-9-]*\\.)+?" + // Subdomains and domain name
339 | "[a-zA-Z]{2,}" + // TLD
340 | "(?:/[^\\s]*)?" + // Optional path
341 | ")(?:\\s+|\$)" // End of string or whitespace
342 | )
343 |
344 | val domainMatch = domainPattern.find(text)
345 | if (domainMatch != null) {
346 | val bareUrl = domainMatch.groupValues[1].trim()
347 | // Add https:// prefix and clean the URL
348 | return cleanUrl("https://$bareUrl")
349 | }
350 |
351 | return null
352 | }
353 |
354 | internal fun cleanUrl(url: String): String {
355 | val cleanedUrl = if (!url.startsWith("http://") && !url.startsWith("https://")) {
356 | val lastHttpsIndex = url.lastIndexOf("https://")
357 | val lastHttpIndex = url.lastIndexOf("http://")
358 | val lastValidUrlIndex = maxOf(lastHttpsIndex, lastHttpIndex)
359 |
360 | if (lastValidUrlIndex != -1) {
361 | // Extract the portion from the last valid protocol and clean any remaining %09 sequences
362 | url.substring(lastValidUrlIndex).replace(Regex("%09+"), "")
363 | } else {
364 | // If no valid protocol is found, add https:// and clean %09 sequences
365 | "https://${url.replace(Regex("%09+"), "")}"
366 | }
367 | } else {
368 | // URL already starts with a protocol, just clean %09 sequences
369 | url.replace(Regex("%09+"), "")
370 | }
371 |
372 | // Remove any trailing punctuation that might have been caught
373 | return cleanedUrl
374 | .removeSuffix(".")
375 | .removeSuffix(",")
376 | .removeSuffix(";")
377 | .removeSuffix(")")
378 | .removeSuffix("'")
379 | .removeSuffix("\"")
380 | }
381 |
382 | open fun openInBrowser(url: String) {
383 | Log.d("MainActivity", "Opening URL: $url")
384 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
385 | startActivity(browserIntent)
386 | finish()
387 | }
388 | }
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/java/org/gnosco/share2archivetoday/WebURLMatcher.kt:
--------------------------------------------------------------------------------
1 | import java.util.regex.Matcher
2 | import java.util.regex.Pattern
3 |
4 | object WebURLMatcher {
5 | private val IP_ADDRESS
6 | : Pattern = Pattern.compile(
7 | "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
8 | + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
9 | + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
10 | + "|[1-9][0-9]|[0-9]))"
11 | )
12 |
13 | /**
14 | * Valid UCS characters defined in RFC 3987. Excludes space characters.
15 | */
16 | private val UCS_CHAR = ("[" +
17 | "\u00A0-\uD7FF" +
18 | "\uF900-\uFDCF" +
19 | "\uFDF0-\uFFEF" +
20 | "\uD800\uDC00-\uD83F\uDFFD" +
21 | "\uD840\uDC00-\uD87F\uDFFD" +
22 | "\uD880\uDC00-\uD8BF\uDFFD" +
23 | "\uD8C0\uDC00-\uD8FF\uDFFD" +
24 | "\uD900\uDC00-\uD93F\uDFFD" +
25 | "\uD940\uDC00-\uD97F\uDFFD" +
26 | "\uD980\uDC00-\uD9BF\uDFFD" +
27 | "\uD9C0\uDC00-\uD9FF\uDFFD" +
28 | "\uDA00\uDC00-\uDA3F\uDFFD" +
29 | "\uDA40\uDC00-\uDA7F\uDFFD" +
30 | "\uDA80\uDC00-\uDABF\uDFFD" +
31 | "\uDAC0\uDC00-\uDAFF\uDFFD" +
32 | "\uDB00\uDC00-\uDB3F\uDFFD" +
33 | "\uDB44\uDC00-\uDB7F\uDFFD" +
34 | "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]")
35 |
36 | /**
37 | * Valid characters for IRI label defined in RFC 3987.
38 | */
39 | private val LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR
40 |
41 | /**
42 | * Valid characters for IRI TLD defined in RFC 3987.
43 | */
44 | private val TLD_CHAR = "a-zA-Z" + UCS_CHAR
45 |
46 | /**
47 | * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
48 | */
49 | private val IRI_LABEL =
50 | "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"
51 |
52 | /**
53 | * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
54 | */
55 | private val PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w"
56 | private val TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" + ")"
57 | private val HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD
58 | val DOMAIN_NAME
59 | : Pattern = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")")
60 | private val PROTOCOL = "(?i:http|https|rtsp)://"
61 |
62 | /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
63 | private val WORD_BOUNDARY = "(?:\\b|$|^)"
64 | private val USER_INFO = (("(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
65 | + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
66 | + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@"))
67 | private val PORT_NUMBER = "\\:\\d{1,5}"
68 | private val PATH_AND_QUERY = ("[/\\?](?:(?:[" + LABEL_CHAR
69 | + ";/\\?:@&=#~" // plus optional query params
70 | + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*")
71 |
72 | /**
73 | * Regular expression pattern to match most part of RFC 3987
74 | * Internationalized URLs, aka IRIs.
75 | */
76 | private val WEB_URL: Pattern = Pattern.compile(
77 | (("("
78 | + "("
79 | + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?"
80 | + "(?:" + DOMAIN_NAME + ")"
81 | + "(?:" + PORT_NUMBER + ")?"
82 | + ")"
83 | + "(" + PATH_AND_QUERY + ")?"
84 | + WORD_BOUNDARY
85 | + ")"))
86 | )
87 |
88 | fun matcher(text: String): Matcher {
89 | return WEB_URL.matcher(text)
90 | }
91 | }
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/play_store_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/app/src/main/res/play_store_512.png
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-af/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Deel met Argief Vandag
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-am/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | አካባቢ ወደ ማህደር ዛሬ
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-ar/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | شارك للأرشيف
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-az-rAZ/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Bu gün Arxivə Paylaş
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-be/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Падзяліцца ў Архіў Сёння
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-bg/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Сподели към Архив Днес
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-bn-rBD/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | আজ আর্কাইভে শেয়ার করুন
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-ca/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Comparteix a l\'Arxiu Avui
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-cs-rCZ/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Sdílet do Archivu Dnes
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-da-rDK/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Del til Arkiv I Dag
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Share 2 Archive Today
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-el-rGR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Μοιράσου στο Αρχείο Σήμερα
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-et/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Jaga Arhiivi Täna
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-eu-rES/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Parteka Artxiboa Gaur
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-fi-rFI/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Jaa Arkistoon Tänään
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-fil/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Ibahagi sa Archive Ngayon
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-fr-rCA/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Partage à l\'Archive
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-fr-rFR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Partage à l\'Archive
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-gl-rES/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Comparte no Arquivo Hoxe
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-gu/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | આજે શેર કરો આર્કાઇવ માટે
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-hi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | आज ही साझा करें आर्काइव में
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-hr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Podijeli u Arhivu Danas
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-hy-rAM/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Կիսվիր Արխիվի հետ այսօր
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-iw/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | שתף לארכיון היום
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-ka/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | გააზიარე არქივში დღესვე
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-my-rMM/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ယနေ့ သိုလှောင်ရန် မျှဝေပါ
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-nl-rNL/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Deel met Archief Vandaag
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-sq/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Ndaj tek Arkiva Sot
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 一键分享存档
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-zh-rHK/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 一鍵分享存檔
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values-zh-rTW/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 一鍵分享存檔
3 |
4 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Share 2 Archive Today
3 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.jetbrains.kotlin.android) apply false
4 | }
--------------------------------------------------------------------------------
/Share2ArchiveToday/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/Share2ArchiveToday/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.9.0"
3 | kotlin = "1.9.0"
4 | coreKtx = "1.10.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.1.5"
7 | espressoCore = "3.5.1"
8 | lifecycleRuntimeKtx = "2.6.1"
9 | activityCompose = "1.8.0"
10 | composeBom = "2024.04.01"
11 | junitJupiter = "5.8.1"
12 |
13 | [libraries]
14 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
15 | junit = { group = "junit", name = "junit", version.ref = "junit" }
16 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
17 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
18 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
19 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
20 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
21 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
22 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
23 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
24 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
25 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
26 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
27 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
28 | junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiter" }
29 |
30 | [plugins]
31 | android-application = { id = "com.android.application", version.ref = "agp" }
32 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
33 |
34 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/Share2ArchiveToday/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Share2ArchiveToday/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jun 18 14:00:19 EDT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/Share2ArchiveToday/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Share 2 Archive Today"
23 | include(":app")
24 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar/full_description.txt:
--------------------------------------------------------------------------------
1 | تطبيق أندرويد يضيف أيقونة لقائمة المشاركة الخاصة بك لأرشفة رابط على Archive.Is و Archive.today
2 |
3 | لا يتجسس عليك و لا يجمع أي بيانات.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar/short_description.txt:
--------------------------------------------------------------------------------
1 | شارك رابط لArchive.today و Archive.Is
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar/title.txt:
--------------------------------------------------------------------------------
1 | شارك للأرشيف
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Simple Android app to add an icon to your share menu to publically archive a url on Archive.today and Archive.Is
2 |
3 | It does not spy on you or collect any data.
4 |
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/0.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/11.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/12.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Share a URL to Archive.today and Archive.Is
2 |
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Share 2 Archive Today
2 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | B094EE2F2D59A0B600F66DEA /* URLShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B094EE252D59A0B600F66DEA /* URLShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
11 | /* End PBXBuildFile section */
12 |
13 | /* Begin PBXContainerItemProxy section */
14 | B094EE2D2D59A0B600F66DEA /* PBXContainerItemProxy */ = {
15 | isa = PBXContainerItemProxy;
16 | containerPortal = B094EE002D5950AA00F66DEA /* Project object */;
17 | proxyType = 1;
18 | remoteGlobalIDString = B094EE242D59A0B600F66DEA;
19 | remoteInfo = URLShareExtension;
20 | };
21 | /* End PBXContainerItemProxy section */
22 |
23 | /* Begin PBXCopyFilesBuildPhase section */
24 | B094EE342D59A0B600F66DEA /* Embed Foundation Extensions */ = {
25 | isa = PBXCopyFilesBuildPhase;
26 | buildActionMask = 2147483647;
27 | dstPath = "";
28 | dstSubfolderSpec = 13;
29 | files = (
30 | B094EE2F2D59A0B600F66DEA /* URLShareExtension.appex in Embed Foundation Extensions */,
31 | );
32 | name = "Embed Foundation Extensions";
33 | runOnlyForDeploymentPostprocessing = 0;
34 | };
35 | /* End PBXCopyFilesBuildPhase section */
36 |
37 | /* Begin PBXFileReference section */
38 | B094EE082D5950AA00F66DEA /* Share-2-Archive-Today.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Share-2-Archive-Today.app"; sourceTree = BUILT_PRODUCTS_DIR; };
39 | B094EE252D59A0B600F66DEA /* URLShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = URLShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
40 | /* End PBXFileReference section */
41 |
42 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
43 | B094EE1A2D5950AD00F66DEA /* Exceptions for "Share-2-Archive-Today" folder in "Share-2-Archive-Today" target */ = {
44 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
45 | membershipExceptions = (
46 | Info.plist,
47 | );
48 | target = B094EE072D5950AA00F66DEA /* Share-2-Archive-Today */;
49 | };
50 | B094EE332D59A0B600F66DEA /* Exceptions for "URLShareExtension" folder in "URLShareExtension" target */ = {
51 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
52 | membershipExceptions = (
53 | Info.plist,
54 | );
55 | target = B094EE242D59A0B600F66DEA /* URLShareExtension */;
56 | };
57 | B094EE3F2D59AB0B00F66DEA /* Exceptions for "Share-2-Archive-Today" folder in "URLShareExtension" target */ = {
58 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
59 | membershipExceptions = (
60 | ArchiveURLService.swift,
61 | URLProcessor.swift,
62 | URLStore.swift,
63 | );
64 | target = B094EE242D59A0B600F66DEA /* URLShareExtension */;
65 | };
66 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
67 |
68 | /* Begin PBXFileSystemSynchronizedRootGroup section */
69 | B094EE0A2D5950AA00F66DEA /* Share-2-Archive-Today */ = {
70 | isa = PBXFileSystemSynchronizedRootGroup;
71 | exceptions = (
72 | B094EE1A2D5950AD00F66DEA /* Exceptions for "Share-2-Archive-Today" folder in "Share-2-Archive-Today" target */,
73 | B094EE3F2D59AB0B00F66DEA /* Exceptions for "Share-2-Archive-Today" folder in "URLShareExtension" target */,
74 | );
75 | path = "Share-2-Archive-Today";
76 | sourceTree = "";
77 | };
78 | B094EE262D59A0B600F66DEA /* URLShareExtension */ = {
79 | isa = PBXFileSystemSynchronizedRootGroup;
80 | exceptions = (
81 | B094EE332D59A0B600F66DEA /* Exceptions for "URLShareExtension" folder in "URLShareExtension" target */,
82 | );
83 | path = URLShareExtension;
84 | sourceTree = "";
85 | };
86 | /* End PBXFileSystemSynchronizedRootGroup section */
87 |
88 | /* Begin PBXFrameworksBuildPhase section */
89 | B094EE052D5950AA00F66DEA /* Frameworks */ = {
90 | isa = PBXFrameworksBuildPhase;
91 | buildActionMask = 2147483647;
92 | files = (
93 | );
94 | runOnlyForDeploymentPostprocessing = 0;
95 | };
96 | B094EE222D59A0B600F66DEA /* Frameworks */ = {
97 | isa = PBXFrameworksBuildPhase;
98 | buildActionMask = 2147483647;
99 | files = (
100 | );
101 | runOnlyForDeploymentPostprocessing = 0;
102 | };
103 | /* End PBXFrameworksBuildPhase section */
104 |
105 | /* Begin PBXGroup section */
106 | B094EDFF2D5950AA00F66DEA = {
107 | isa = PBXGroup;
108 | children = (
109 | B094EE0A2D5950AA00F66DEA /* Share-2-Archive-Today */,
110 | B094EE262D59A0B600F66DEA /* URLShareExtension */,
111 | B094EE092D5950AA00F66DEA /* Products */,
112 | );
113 | sourceTree = "";
114 | };
115 | B094EE092D5950AA00F66DEA /* Products */ = {
116 | isa = PBXGroup;
117 | children = (
118 | B094EE082D5950AA00F66DEA /* Share-2-Archive-Today.app */,
119 | B094EE252D59A0B600F66DEA /* URLShareExtension.appex */,
120 | );
121 | name = Products;
122 | sourceTree = "";
123 | };
124 | /* End PBXGroup section */
125 |
126 | /* Begin PBXNativeTarget section */
127 | B094EE072D5950AA00F66DEA /* Share-2-Archive-Today */ = {
128 | isa = PBXNativeTarget;
129 | buildConfigurationList = B094EE1B2D5950AD00F66DEA /* Build configuration list for PBXNativeTarget "Share-2-Archive-Today" */;
130 | buildPhases = (
131 | B094EE042D5950AA00F66DEA /* Sources */,
132 | B094EE052D5950AA00F66DEA /* Frameworks */,
133 | B094EE062D5950AA00F66DEA /* Resources */,
134 | B094EE342D59A0B600F66DEA /* Embed Foundation Extensions */,
135 | );
136 | buildRules = (
137 | );
138 | dependencies = (
139 | B094EE2E2D59A0B600F66DEA /* PBXTargetDependency */,
140 | );
141 | fileSystemSynchronizedGroups = (
142 | B094EE0A2D5950AA00F66DEA /* Share-2-Archive-Today */,
143 | );
144 | name = "Share-2-Archive-Today";
145 | packageProductDependencies = (
146 | );
147 | productName = "Share-2-Archive-Today";
148 | productReference = B094EE082D5950AA00F66DEA /* Share-2-Archive-Today.app */;
149 | productType = "com.apple.product-type.application";
150 | };
151 | B094EE242D59A0B600F66DEA /* URLShareExtension */ = {
152 | isa = PBXNativeTarget;
153 | buildConfigurationList = B094EE302D59A0B600F66DEA /* Build configuration list for PBXNativeTarget "URLShareExtension" */;
154 | buildPhases = (
155 | B094EE212D59A0B600F66DEA /* Sources */,
156 | B094EE222D59A0B600F66DEA /* Frameworks */,
157 | B094EE232D59A0B600F66DEA /* Resources */,
158 | );
159 | buildRules = (
160 | );
161 | dependencies = (
162 | );
163 | fileSystemSynchronizedGroups = (
164 | B094EE262D59A0B600F66DEA /* URLShareExtension */,
165 | );
166 | name = URLShareExtension;
167 | packageProductDependencies = (
168 | );
169 | productName = URLShareExtension;
170 | productReference = B094EE252D59A0B600F66DEA /* URLShareExtension.appex */;
171 | productType = "com.apple.product-type.app-extension";
172 | };
173 | /* End PBXNativeTarget section */
174 |
175 | /* Begin PBXProject section */
176 | B094EE002D5950AA00F66DEA /* Project object */ = {
177 | isa = PBXProject;
178 | attributes = {
179 | BuildIndependentTargetsInParallel = 1;
180 | LastSwiftUpdateCheck = 1610;
181 | LastUpgradeCheck = 1610;
182 | TargetAttributes = {
183 | B094EE072D5950AA00F66DEA = {
184 | CreatedOnToolsVersion = 16.1;
185 | };
186 | B094EE242D59A0B600F66DEA = {
187 | CreatedOnToolsVersion = 16.1;
188 | };
189 | };
190 | };
191 | buildConfigurationList = B094EE032D5950AA00F66DEA /* Build configuration list for PBXProject "Share-2-Archive-Today" */;
192 | developmentRegion = en;
193 | hasScannedForEncodings = 0;
194 | knownRegions = (
195 | en,
196 | Base,
197 | );
198 | mainGroup = B094EDFF2D5950AA00F66DEA;
199 | minimizedProjectReferenceProxies = 1;
200 | preferredProjectObjectVersion = 77;
201 | productRefGroup = B094EE092D5950AA00F66DEA /* Products */;
202 | projectDirPath = "";
203 | projectRoot = "";
204 | targets = (
205 | B094EE072D5950AA00F66DEA /* Share-2-Archive-Today */,
206 | B094EE242D59A0B600F66DEA /* URLShareExtension */,
207 | );
208 | };
209 | /* End PBXProject section */
210 |
211 | /* Begin PBXResourcesBuildPhase section */
212 | B094EE062D5950AA00F66DEA /* Resources */ = {
213 | isa = PBXResourcesBuildPhase;
214 | buildActionMask = 2147483647;
215 | files = (
216 | );
217 | runOnlyForDeploymentPostprocessing = 0;
218 | };
219 | B094EE232D59A0B600F66DEA /* Resources */ = {
220 | isa = PBXResourcesBuildPhase;
221 | buildActionMask = 2147483647;
222 | files = (
223 | );
224 | runOnlyForDeploymentPostprocessing = 0;
225 | };
226 | /* End PBXResourcesBuildPhase section */
227 |
228 | /* Begin PBXSourcesBuildPhase section */
229 | B094EE042D5950AA00F66DEA /* Sources */ = {
230 | isa = PBXSourcesBuildPhase;
231 | buildActionMask = 2147483647;
232 | files = (
233 | );
234 | runOnlyForDeploymentPostprocessing = 0;
235 | };
236 | B094EE212D59A0B600F66DEA /* Sources */ = {
237 | isa = PBXSourcesBuildPhase;
238 | buildActionMask = 2147483647;
239 | files = (
240 | );
241 | runOnlyForDeploymentPostprocessing = 0;
242 | };
243 | /* End PBXSourcesBuildPhase section */
244 |
245 | /* Begin PBXTargetDependency section */
246 | B094EE2E2D59A0B600F66DEA /* PBXTargetDependency */ = {
247 | isa = PBXTargetDependency;
248 | target = B094EE242D59A0B600F66DEA /* URLShareExtension */;
249 | targetProxy = B094EE2D2D59A0B600F66DEA /* PBXContainerItemProxy */;
250 | };
251 | /* End PBXTargetDependency section */
252 |
253 | /* Begin XCBuildConfiguration section */
254 | B094EE1C2D5950AD00F66DEA /* Debug */ = {
255 | isa = XCBuildConfiguration;
256 | buildSettings = {
257 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
258 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
259 | CODE_SIGN_ENTITLEMENTS = "Share-2-Archive-Today/Share-2-Archive-Today.entitlements";
260 | CODE_SIGN_STYLE = Automatic;
261 | CURRENT_PROJECT_VERSION = 30;
262 | DEVELOPMENT_TEAM = NR87AJVD3K;
263 | GENERATE_INFOPLIST_FILE = YES;
264 | INFOPLIST_FILE = "Share-2-Archive-Today/Info.plist";
265 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
266 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
267 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
268 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
269 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
270 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
271 | LD_RUNPATH_SEARCH_PATHS = (
272 | "$(inherited)",
273 | "@executable_path/Frameworks",
274 | );
275 | MARKETING_VERSION = 3.0;
276 | PRODUCT_BUNDLE_IDENTIFIER = "org.Gnosco.Share-2-Archive-Today";
277 | PRODUCT_NAME = "$(TARGET_NAME)";
278 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
279 | SUPPORTS_MACCATALYST = NO;
280 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
281 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
282 | SWIFT_EMIT_LOC_STRINGS = YES;
283 | SWIFT_VERSION = 5.0;
284 | TARGETED_DEVICE_FAMILY = "1,2";
285 | };
286 | name = Debug;
287 | };
288 | B094EE1D2D5950AD00F66DEA /* Release */ = {
289 | isa = XCBuildConfiguration;
290 | buildSettings = {
291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
292 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
293 | CODE_SIGN_ENTITLEMENTS = "Share-2-Archive-Today/Share-2-Archive-Today.entitlements";
294 | CODE_SIGN_STYLE = Automatic;
295 | CURRENT_PROJECT_VERSION = 30;
296 | DEVELOPMENT_TEAM = NR87AJVD3K;
297 | GENERATE_INFOPLIST_FILE = YES;
298 | INFOPLIST_FILE = "Share-2-Archive-Today/Info.plist";
299 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
300 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
301 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
304 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
305 | LD_RUNPATH_SEARCH_PATHS = (
306 | "$(inherited)",
307 | "@executable_path/Frameworks",
308 | );
309 | MARKETING_VERSION = 3.0;
310 | PRODUCT_BUNDLE_IDENTIFIER = "org.Gnosco.Share-2-Archive-Today";
311 | PRODUCT_NAME = "$(TARGET_NAME)";
312 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
313 | SUPPORTS_MACCATALYST = NO;
314 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
315 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
316 | SWIFT_EMIT_LOC_STRINGS = YES;
317 | SWIFT_VERSION = 5.0;
318 | TARGETED_DEVICE_FAMILY = "1,2";
319 | };
320 | name = Release;
321 | };
322 | B094EE1E2D5950AD00F66DEA /* Debug */ = {
323 | isa = XCBuildConfiguration;
324 | buildSettings = {
325 | ALWAYS_SEARCH_USER_PATHS = NO;
326 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
327 | CLANG_ANALYZER_NONNULL = YES;
328 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
330 | CLANG_ENABLE_MODULES = YES;
331 | CLANG_ENABLE_OBJC_ARC = YES;
332 | CLANG_ENABLE_OBJC_WEAK = YES;
333 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
334 | CLANG_WARN_BOOL_CONVERSION = YES;
335 | CLANG_WARN_COMMA = YES;
336 | CLANG_WARN_CONSTANT_CONVERSION = YES;
337 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
338 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
339 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
340 | CLANG_WARN_EMPTY_BODY = YES;
341 | CLANG_WARN_ENUM_CONVERSION = YES;
342 | CLANG_WARN_INFINITE_RECURSION = YES;
343 | CLANG_WARN_INT_CONVERSION = YES;
344 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
345 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
346 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
348 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
349 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
350 | CLANG_WARN_STRICT_PROTOTYPES = YES;
351 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
352 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
353 | CLANG_WARN_UNREACHABLE_CODE = YES;
354 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
355 | COPY_PHASE_STRIP = NO;
356 | DEBUG_INFORMATION_FORMAT = dwarf;
357 | ENABLE_STRICT_OBJC_MSGSEND = YES;
358 | ENABLE_TESTABILITY = YES;
359 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
360 | GCC_C_LANGUAGE_STANDARD = gnu17;
361 | GCC_DYNAMIC_NO_PIC = NO;
362 | GCC_NO_COMMON_BLOCKS = YES;
363 | GCC_OPTIMIZATION_LEVEL = 0;
364 | GCC_PREPROCESSOR_DEFINITIONS = (
365 | "DEBUG=1",
366 | "$(inherited)",
367 | );
368 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
369 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
370 | GCC_WARN_UNDECLARED_SELECTOR = YES;
371 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
372 | GCC_WARN_UNUSED_FUNCTION = YES;
373 | GCC_WARN_UNUSED_VARIABLE = YES;
374 | IPHONEOS_DEPLOYMENT_TARGET = 18.1;
375 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
376 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
377 | MTL_FAST_MATH = YES;
378 | ONLY_ACTIVE_ARCH = YES;
379 | SDKROOT = iphoneos;
380 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
381 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
382 | };
383 | name = Debug;
384 | };
385 | B094EE1F2D5950AD00F66DEA /* Release */ = {
386 | isa = XCBuildConfiguration;
387 | buildSettings = {
388 | ALWAYS_SEARCH_USER_PATHS = NO;
389 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
390 | CLANG_ANALYZER_NONNULL = YES;
391 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
392 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
393 | CLANG_ENABLE_MODULES = YES;
394 | CLANG_ENABLE_OBJC_ARC = YES;
395 | CLANG_ENABLE_OBJC_WEAK = YES;
396 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
397 | CLANG_WARN_BOOL_CONVERSION = YES;
398 | CLANG_WARN_COMMA = YES;
399 | CLANG_WARN_CONSTANT_CONVERSION = YES;
400 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
401 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
402 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
403 | CLANG_WARN_EMPTY_BODY = YES;
404 | CLANG_WARN_ENUM_CONVERSION = YES;
405 | CLANG_WARN_INFINITE_RECURSION = YES;
406 | CLANG_WARN_INT_CONVERSION = YES;
407 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
408 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
409 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
411 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
412 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
413 | CLANG_WARN_STRICT_PROTOTYPES = YES;
414 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
415 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
416 | CLANG_WARN_UNREACHABLE_CODE = YES;
417 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
418 | COPY_PHASE_STRIP = NO;
419 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
420 | ENABLE_NS_ASSERTIONS = NO;
421 | ENABLE_STRICT_OBJC_MSGSEND = YES;
422 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
423 | GCC_C_LANGUAGE_STANDARD = gnu17;
424 | GCC_NO_COMMON_BLOCKS = YES;
425 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
426 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
427 | GCC_WARN_UNDECLARED_SELECTOR = YES;
428 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
429 | GCC_WARN_UNUSED_FUNCTION = YES;
430 | GCC_WARN_UNUSED_VARIABLE = YES;
431 | IPHONEOS_DEPLOYMENT_TARGET = 18.1;
432 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
433 | MTL_ENABLE_DEBUG_INFO = NO;
434 | MTL_FAST_MATH = YES;
435 | SDKROOT = iphoneos;
436 | SWIFT_COMPILATION_MODE = wholemodule;
437 | VALIDATE_PRODUCT = YES;
438 | };
439 | name = Release;
440 | };
441 | B094EE312D59A0B600F66DEA /* Debug */ = {
442 | isa = XCBuildConfiguration;
443 | buildSettings = {
444 | CODE_SIGN_ENTITLEMENTS = URLShareExtension/URLShareExtension.entitlements;
445 | CODE_SIGN_STYLE = Automatic;
446 | CURRENT_PROJECT_VERSION = 30;
447 | DEVELOPMENT_TEAM = NR87AJVD3K;
448 | GENERATE_INFOPLIST_FILE = YES;
449 | INFOPLIST_FILE = URLShareExtension/Info.plist;
450 | INFOPLIST_KEY_CFBundleDisplayName = URLShareExtension;
451 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
452 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
453 | LD_RUNPATH_SEARCH_PATHS = (
454 | "$(inherited)",
455 | "@executable_path/Frameworks",
456 | "@executable_path/../../Frameworks",
457 | );
458 | MARKETING_VERSION = 3.0;
459 | PRODUCT_BUNDLE_IDENTIFIER = "org.Gnosco.Share-2-Archive-Today.URLShareExtension";
460 | PRODUCT_NAME = "$(TARGET_NAME)";
461 | SKIP_INSTALL = YES;
462 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
463 | SUPPORTS_MACCATALYST = NO;
464 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
465 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
466 | SWIFT_EMIT_LOC_STRINGS = YES;
467 | SWIFT_VERSION = 5.0;
468 | TARGETED_DEVICE_FAMILY = "1,2";
469 | };
470 | name = Debug;
471 | };
472 | B094EE322D59A0B600F66DEA /* Release */ = {
473 | isa = XCBuildConfiguration;
474 | buildSettings = {
475 | CODE_SIGN_ENTITLEMENTS = URLShareExtension/URLShareExtension.entitlements;
476 | CODE_SIGN_STYLE = Automatic;
477 | CURRENT_PROJECT_VERSION = 30;
478 | DEVELOPMENT_TEAM = NR87AJVD3K;
479 | GENERATE_INFOPLIST_FILE = YES;
480 | INFOPLIST_FILE = URLShareExtension/Info.plist;
481 | INFOPLIST_KEY_CFBundleDisplayName = URLShareExtension;
482 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
483 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
484 | LD_RUNPATH_SEARCH_PATHS = (
485 | "$(inherited)",
486 | "@executable_path/Frameworks",
487 | "@executable_path/../../Frameworks",
488 | );
489 | MARKETING_VERSION = 3.0;
490 | PRODUCT_BUNDLE_IDENTIFIER = "org.Gnosco.Share-2-Archive-Today.URLShareExtension";
491 | PRODUCT_NAME = "$(TARGET_NAME)";
492 | SKIP_INSTALL = YES;
493 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
494 | SUPPORTS_MACCATALYST = NO;
495 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
496 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
497 | SWIFT_EMIT_LOC_STRINGS = YES;
498 | SWIFT_VERSION = 5.0;
499 | TARGETED_DEVICE_FAMILY = "1,2";
500 | };
501 | name = Release;
502 | };
503 | /* End XCBuildConfiguration section */
504 |
505 | /* Begin XCConfigurationList section */
506 | B094EE032D5950AA00F66DEA /* Build configuration list for PBXProject "Share-2-Archive-Today" */ = {
507 | isa = XCConfigurationList;
508 | buildConfigurations = (
509 | B094EE1E2D5950AD00F66DEA /* Debug */,
510 | B094EE1F2D5950AD00F66DEA /* Release */,
511 | );
512 | defaultConfigurationIsVisible = 0;
513 | defaultConfigurationName = Release;
514 | };
515 | B094EE1B2D5950AD00F66DEA /* Build configuration list for PBXNativeTarget "Share-2-Archive-Today" */ = {
516 | isa = XCConfigurationList;
517 | buildConfigurations = (
518 | B094EE1C2D5950AD00F66DEA /* Debug */,
519 | B094EE1D2D5950AD00F66DEA /* Release */,
520 | );
521 | defaultConfigurationIsVisible = 0;
522 | defaultConfigurationName = Release;
523 | };
524 | B094EE302D59A0B600F66DEA /* Build configuration list for PBXNativeTarget "URLShareExtension" */ = {
525 | isa = XCConfigurationList;
526 | buildConfigurations = (
527 | B094EE312D59A0B600F66DEA /* Debug */,
528 | B094EE322D59A0B600F66DEA /* Release */,
529 | );
530 | defaultConfigurationIsVisible = 0;
531 | defaultConfigurationName = Release;
532 | };
533 | /* End XCConfigurationList section */
534 | };
535 | rootObject = B094EE002D5950AA00F66DEA /* Project object */;
536 | }
537 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/xcshareddata/xcschemes/Share-2-Archive-Today.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/xcshareddata/xcschemes/URLShareExtension.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
11 |
17 |
23 |
24 |
25 |
31 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
60 |
62 |
68 |
69 |
70 |
71 |
79 |
81 |
87 |
88 |
89 |
90 |
92 |
93 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today.xcodeproj/xcuserdata/gabirelfair.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Share-2-Archive-Today.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | URLShareExtension.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | B094EE072D5950AA00F66DEA
21 |
22 | primary
23 |
24 |
25 | B094EE242D59A0B600F66DEA
26 |
27 | primary
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Share-2-Archive-Today
4 | //
5 | import UIKit
6 | import SafariServices
7 | import os.log
8 |
9 | @main
10 | class AppDelegate: UIResponder, UIApplicationDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | /// The URL that was used to launch the app, if any
15 | private var launchURL: URL?
16 |
17 | /// Logger for debugging
18 | private let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "AppDelegate")
19 |
20 | /// Archive URL service
21 | private let archiveService = ArchiveURLService.shared
22 |
23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
24 | // Check if the app was launched from a URL
25 | if let url = launchOptions?[.url] as? URL {
26 | logger.info("App launched with URL: \(url)")
27 | launchURL = url
28 | }
29 |
30 | // Setup any required app configurations
31 | setupAppConfiguration()
32 |
33 | return true
34 | }
35 |
36 | // MARK: - URL Handling
37 |
38 | func application(_ app: UIApplication,
39 | open url: URL,
40 | options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
41 | // Handle URLs coming from the share extension
42 | logger.info("App opened with URL: \(url)")
43 |
44 | // Get the root view controller
45 | guard let rootViewController = window?.rootViewController else {
46 | logger.error("Could not get root view controller")
47 | return false
48 | }
49 |
50 | // Don't try to create a UIOpenURLContext, instead modify the ArchiveURLService
51 | // to have a method that directly handles URLs
52 | if url.scheme == "share2archivetoday" {
53 | if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
54 | let queryItem = components.queryItems?.first(where: { $0.name == "url" }),
55 | let receivedUrlString = queryItem.value,
56 | let decodedURL = receivedUrlString.removingPercentEncoding {
57 |
58 | // Process the URL directly
59 | archiveService.processAndArchiveURL(decodedURL, from: rootViewController)
60 | } else {
61 | // No URL in parameters, check for pending URL
62 | archiveService.checkForPendingURL(from: rootViewController)
63 | }
64 | } else {
65 | logger.error("Unsupported URL scheme: \(url.scheme ?? "nil")")
66 | }
67 |
68 | return true
69 | }
70 |
71 | // MARK: - App Configuration
72 |
73 | private func setupAppConfiguration() {
74 | logger.info("Setting up app configuration")
75 |
76 | // Setup any required services
77 | setupServices()
78 | }
79 |
80 | private func setupServices() {
81 | // Initialize URLStore
82 | _ = URLStore.shared
83 | logger.debug("URLStore initialized")
84 |
85 | // Initialize Archive URL Service
86 | _ = ArchiveURLService.shared
87 | logger.debug("ArchiveURLService initialized")
88 |
89 | // Setup any other required services
90 | }
91 |
92 | // MARK: - UISceneSession Lifecycle
93 |
94 | func application(_ application: UIApplication,
95 | configurationForConnecting connectingSceneSession: UISceneSession,
96 | options: UIScene.ConnectionOptions) -> UISceneConfiguration {
97 | logger.debug("Configuring scene session: \(String(describing: connectingSceneSession.role))")
98 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
99 | }
100 |
101 | func application(_ application: UIApplication,
102 | didDiscardSceneSessions sceneSessions: Set) {
103 | // Handle any cleanup when scenes are discarded
104 | logger.debug("Discarded scene sessions: \(sceneSessions.count)")
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/ArchiveURLService.swift:
--------------------------------------------------------------------------------
1 | // ArchiveURLService.swift
2 | import UIKit
3 | import SafariServices
4 | import os.log
5 |
6 | /// A service to handle URL processing and archiving through archive.today
7 | class ArchiveURLService {
8 | static let shared = ArchiveURLService()
9 |
10 | /// Logger for debugging
11 | private let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "ArchiveURLService")
12 |
13 | /// URL store for saving URLs
14 | private let urlStore = URLStore.shared
15 |
16 | /// Private initializer for singleton pattern
17 | private init() {}
18 |
19 | /// Processes a URL for archiving, cleaning it and storing it
20 | /// - Parameter urlString: Original URL to process
21 | /// - Returns: The processed URL string
22 | func processURL(_ urlString: String, saveToStore: Bool = false) -> String {
23 | logger.info("Processing URL for archiving: \(urlString)")
24 |
25 | // Use the URLProcessor to clean tracking parameters
26 | let processedURL = URLProcessor.processURL(urlString)
27 | logger.info("URL processed: \(urlString) -> \(processedURL)")
28 |
29 | // Only save if explicitly requested
30 | if saveToStore {
31 | urlStore.saveURL(processedURL)
32 | logger.info("Processed URL saved to store: \(processedURL)")
33 | }
34 |
35 | return processedURL
36 | }
37 |
38 | func saveURL(_ urlString: String) {
39 | urlStore.saveURL(urlString)
40 | logger.info("URL saved to store: \(urlString)")
41 | }
42 |
43 | /// Creates an archive.today URL from a given URL string
44 | /// - Parameter urlString: The URL to archive
45 | /// - Returns: A URL for the archive.today service, if creation was successful
46 | func createArchiveURL(from urlString: String) -> URL? {
47 | guard let encodedUrl = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
48 | logger.error("Failed to encode URL: \(urlString)")
49 | return nil
50 | }
51 |
52 | let archiveUrlString = "https://archive.today/?run=1&url=\(encodedUrl)"
53 | logger.debug("Created archive URL: \(archiveUrlString)")
54 | return URL(string: archiveUrlString)
55 | }
56 |
57 | /// Presents the archive.today page in a Safari View Controller
58 | /// - Parameters:
59 | /// - url: The archive.today URL to present
60 | /// - viewController: The view controller from which to present
61 | func presentArchivePage(with url: URL, from viewController: UIViewController) {
62 | let safariVC = SFSafariViewController(url: url)
63 | safariVC.preferredControlTintColor = .systemBlue
64 |
65 | logger.info("Presenting Safari View Controller with URL: \(url)")
66 |
67 | if let presented = viewController.presentedViewController {
68 | logger.debug("Dismissing existing presented view controller")
69 | presented.dismiss(animated: true) {
70 | viewController.present(safariVC, animated: true)
71 | }
72 | } else {
73 | viewController.present(safariVC, animated: true)
74 | }
75 | }
76 |
77 | /// Complete URL processing and archiving workflow
78 | /// - Parameters:
79 | /// - urlString: The original URL string
80 | /// - viewController: The view controller to present from
81 | /// - Returns: The processed URL string
82 | func processAndArchiveURL(_ urlString: String, from viewController: UIViewController) -> String {
83 | // Process the URL
84 | let processedURL = processURL(urlString)
85 |
86 | // Create and present archive.today URL
87 | if let archiveURL = createArchiveURL(from: processedURL) {
88 | presentArchivePage(with: archiveURL, from: viewController)
89 | } else {
90 | logger.error("Failed to create archive URL from: \(processedURL)")
91 | // Show an error alert
92 | let alert = UIAlertController(
93 | title: "Error",
94 | message: "Could not create archive URL",
95 | preferredStyle: .alert
96 | )
97 | alert.addAction(UIAlertAction(title: "OK", style: .default))
98 | viewController.present(alert, animated: true)
99 | }
100 |
101 | return processedURL
102 | }
103 |
104 | /// Extracts a URL from a URL context and processes it
105 | /// - Parameters:
106 | /// - urlContext: The URL context containing the URL
107 | /// - viewController: The view controller to present from
108 | func handleURLContext(_ urlContext: UIOpenURLContext, from viewController: UIViewController) {
109 | let incomingURL = urlContext.url
110 | logger.info("Received URL in context: \(incomingURL)")
111 |
112 | if incomingURL.scheme == "share2archivetoday" {
113 | if let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
114 | let queryItem = components.queryItems?.first(where: { $0.name == "url" }),
115 | let receivedUrlString = queryItem.value,
116 | let decodedURL = receivedUrlString.removingPercentEncoding {
117 |
118 | // Process the URL directly from the URL parameters
119 | processAndArchiveURL(decodedURL, from: viewController)
120 | } else {
121 | // No URL in parameters, check if we need to process a pending URL
122 | checkForPendingURL(from: viewController)
123 | }
124 | } else {
125 | logger.error("Unsupported URL scheme: \(incomingURL.scheme ?? "nil")")
126 | }
127 | }
128 |
129 | /// Checks if there's a pending URL from the share extension that needs to be processed
130 | /// - Parameter viewController: The view controller to present from
131 | func checkForPendingURL(from viewController: UIViewController) {
132 | logger.info("Checking for pending URL from share extension")
133 |
134 | if let defaults = UserDefaults(suiteName: "group.org.Gnosco.Share-2-Archive-Today") {
135 | let hasPendingURL = defaults.bool(forKey: "pendingArchiveURL")
136 |
137 | if hasPendingURL, let urlString = defaults.string(forKey: "lastSharedURL") {
138 | logger.info("Found pending URL: \(urlString)")
139 |
140 | // Clear the pending flag
141 | defaults.set(false, forKey: "pendingArchiveURL")
142 |
143 | // Process the URL
144 | processAndArchiveURL(urlString, from: viewController)
145 | } else {
146 | logger.info("No pending URL found")
147 | }
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ],
16 | "idiom" : "universal",
17 | "platform" : "ios",
18 | "size" : "1024x1024"
19 | },
20 | {
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "tinted"
25 | }
26 | ],
27 | "idiom" : "universal",
28 | "platform" : "ios",
29 | "size" : "1024x1024"
30 | }
31 | ],
32 | "info" : {
33 | "author" : "xcode",
34 | "version" : 1
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Assets.xcassets/AppIcon.appiconset/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabefair/Share-2-Archive-Today/40d2b86f32cbf913f6d6619546fc80d5afca9aa2/ios/Share-2-Archive-Today/Share-2-Archive-Today/Assets.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
32 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
86 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/FAQController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SafariServices
3 | import os.log
4 |
5 | /// A view controller that displays an FAQ and contact information
6 | class FAQViewController: UIViewController {
7 |
8 | // MARK: - Properties
9 |
10 | private let scrollView = UIScrollView()
11 | private let contentView = UIView()
12 | private let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "FAQViewController")
13 |
14 | // MARK: - Lifecycle Methods
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | title = "Help & FAQ"
20 | view.backgroundColor = .systemBackground
21 |
22 | setupScrollView()
23 | setupContentView()
24 |
25 | // Add close button
26 | navigationItem.leftBarButtonItem = UIBarButtonItem(
27 | barButtonSystemItem: .close,
28 | target: self,
29 | action: #selector(dismissViewController)
30 | )
31 |
32 | logger.debug("FAQ view controller loaded")
33 | }
34 |
35 | // MARK: - Setup Methods
36 |
37 | private func setupScrollView() {
38 | view.addSubview(scrollView)
39 | scrollView.translatesAutoresizingMaskIntoConstraints = false
40 |
41 | NSLayoutConstraint.activate([
42 | scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
43 | scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
44 | scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
45 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
46 | ])
47 | }
48 |
49 | private func setupContentView() {
50 | scrollView.addSubview(contentView)
51 | contentView.translatesAutoresizingMaskIntoConstraints = false
52 |
53 | NSLayoutConstraint.activate([
54 | contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
55 | contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
56 | contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
57 | contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
58 | contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
59 | ])
60 |
61 | let stackView = UIStackView()
62 | stackView.axis = .vertical
63 | stackView.spacing = 24
64 | stackView.alignment = .fill
65 | stackView.translatesAutoresizingMaskIntoConstraints = false
66 |
67 | contentView.addSubview(stackView)
68 |
69 | NSLayoutConstraint.activate([
70 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24),
71 | stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
72 | stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
73 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24)
74 | ])
75 |
76 | // Add FAQ items
77 | addFAQItem(to: stackView, question: "What is Share 2 Archive.today?", answer: "This app allows you to easily archive web pages using archive.today service. It helps preserve web content that might change or disappear in the future. Please know that archived websites CAN NOT be deleted.")
78 |
79 | addFAQItem(to: stackView, question: "How do I archive a URL?", answer: "There are several ways:\n\n1. Use the share button in Safari or other apps and select 'Share 2 Archive.today'\n2. Share text containing a URL\n3. Share an image containing a QR code with a URL")
80 |
81 | addFAQItem(to: stackView, question: "Using a VPN?", answer: "If you are uisng a VPN, you might have to solve many captchas, adding this app to your VPN whitelist or split tunnel might help.")
82 |
83 | addFAQItem(to: stackView, question: "Why archive URLs?", answer: "Archiving preserves web content in its current state, protecting against changes, removal or link rot. It's useful for research, citations, and preserving digital content.")
84 |
85 | addFAQItem(to: stackView, question: "What happens to the URLs I archive?", answer: "The URLs are sent to archive.today for archiving. A copy of the URL is saved in this app for your reference, but the actual archived content is stored on archive.today's servers.")
86 |
87 | addFAQItem(to: stackView, question: "Can I archive any URL?", answer: "Most URLs can be archived, but some websites may block archiving services. Additionally, websites requiring login or with dynamic content may not archive properly.")
88 |
89 | addFAQItem(to: stackView, question: "Can I see the source code?", answer: "Yes, it can be found here: https://github.com/gabefair/share-2-archive-today")
90 |
91 | addFAQItem(to: stackView, question: "How can I help the project?", answer: "You can help by donating to the Archive.Today project, also using the Chrome/Firefox/Edge extension, or contributing to the source code.")
92 |
93 | addFAQItem(to: stackView, question: "How do I contact support?", answer: "If you have any issues or questions, please contact us at support@gnosco.org.")
94 |
95 | // Add contact button
96 | let contactButton = UIButton(type: .system)
97 | contactButton.setTitle("Contact Support", for: .normal)
98 | contactButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
99 | contactButton.backgroundColor = .systemBlue
100 | contactButton.setTitleColor(.white, for: .normal)
101 | contactButton.layer.cornerRadius = 12
102 | contactButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 20, bottom: 12, right: 20)
103 | contactButton.addTarget(self, action: #selector(contactSupport), for: .touchUpInside)
104 | contactButton.translatesAutoresizingMaskIntoConstraints = false
105 |
106 | let contactContainer = UIView()
107 | contactContainer.addSubview(contactButton)
108 |
109 | NSLayoutConstraint.activate([
110 | contactButton.topAnchor.constraint(equalTo: contactContainer.topAnchor, constant: 20),
111 | contactButton.centerXAnchor.constraint(equalTo: contactContainer.centerXAnchor),
112 | contactButton.bottomAnchor.constraint(equalTo: contactContainer.bottomAnchor, constant: -20)
113 | ])
114 |
115 | stackView.addArrangedSubview(contactContainer)
116 | }
117 |
118 | private func addFAQItem(to stackView: UIStackView, question: String, answer: String) {
119 | let containerView = UIView()
120 |
121 | let questionLabel = UILabel()
122 | questionLabel.text = question
123 | questionLabel.font = UIFont.systemFont(ofSize: 18, weight: .bold)
124 | questionLabel.textColor = .label
125 | questionLabel.numberOfLines = 0
126 |
127 | let answerLabel = UILabel()
128 | answerLabel.text = answer
129 | answerLabel.font = UIFont.systemFont(ofSize: 16)
130 | answerLabel.textColor = .secondaryLabel
131 | answerLabel.numberOfLines = 0
132 |
133 | containerView.addSubview(questionLabel)
134 | containerView.addSubview(answerLabel)
135 |
136 | questionLabel.translatesAutoresizingMaskIntoConstraints = false
137 | answerLabel.translatesAutoresizingMaskIntoConstraints = false
138 |
139 | NSLayoutConstraint.activate([
140 | questionLabel.topAnchor.constraint(equalTo: containerView.topAnchor),
141 | questionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
142 | questionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
143 |
144 | answerLabel.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 8),
145 | answerLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
146 | answerLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
147 | answerLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
148 | ])
149 |
150 | stackView.addArrangedSubview(containerView)
151 | }
152 |
153 | // MARK: - Action Methods
154 |
155 | @objc private func dismissViewController() {
156 | logger.debug("Dismissing FAQ view controller")
157 | dismiss(animated: true)
158 | }
159 |
160 | @objc private func contactSupport() {
161 | logger.info("Opening mail app for support contact")
162 | if let url = URL(string: "mailto:support@gnosco.org") {
163 | UIApplication.shared.open(url)
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/FloatingButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingButton.swift
3 | // Share-2-Archive-Today
4 |
5 | import UIKit
6 |
7 | /// A custom floating button that appears in the corner of the screen
8 | class FloatingButton: UIButton {
9 |
10 | override init(frame: CGRect) {
11 | super.init(frame: frame)
12 | setupButton()
13 | }
14 |
15 | required init?(coder: NSCoder) {
16 | super.init(coder: coder)
17 | setupButton()
18 | }
19 |
20 | private func setupButton() {
21 | backgroundColor = .systemBlue
22 | layer.cornerRadius = 30
23 | layer.shadowColor = UIColor.black.cgColor
24 | layer.shadowOffset = CGSize(width: 0, height: 2)
25 | layer.shadowOpacity = 0.3
26 | layer.shadowRadius = 4
27 |
28 | // Set the question mark image
29 | let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)
30 | setImage(UIImage(systemName: "questionmark", withConfiguration: config), for: .normal)
31 | tintColor = .white
32 |
33 | // Add constraints
34 | translatesAutoresizingMaskIntoConstraints = false
35 | widthAnchor.constraint(equalToConstant: 60).isActive = true
36 | heightAnchor.constraint(equalToConstant: 60).isActive = true
37 | }
38 |
39 | override func layoutSubviews() {
40 | super.layoutSubviews()
41 | layer.cornerRadius = bounds.height / 2
42 | }
43 |
44 | // Add slight animation on touch
45 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
46 | super.touchesBegan(touches, with: event)
47 | UIView.animate(withDuration: 0.1) {
48 | self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
49 | }
50 | }
51 |
52 | override func touchesEnded(_ touches: Set, with event: UIEvent?) {
53 | super.touchesEnded(touches, with: event)
54 | UIView.animate(withDuration: 0.1) {
55 | self.transform = CGAffineTransform.identity
56 | }
57 | }
58 |
59 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
60 | super.touchesCancelled(touches, with: event)
61 | UIView.animate(withDuration: 0.1) {
62 | self.transform = CGAffineTransform.identity
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ITSAppUsesNonExemptEncryption
6 |
7 | CFBundleURLTypes
8 |
9 |
10 | CFBundleURLName
11 | org.Gnosco.Share-2-Archive-Today
12 | CFBundleURLSchemes
13 |
14 | share2archivetoday
15 |
16 | CFBundleTypeRole
17 | Editor
18 |
19 |
20 | UIApplicationSceneManifest
21 |
22 | UIApplicationSupportsMultipleScenes
23 |
24 | UISceneConfigurations
25 |
26 | UIWindowSceneSessionRoleApplication
27 |
28 |
29 | UISceneConfigurationName
30 | Default Configuration
31 | UISceneDelegateClassName
32 | $(PRODUCT_MODULE_NAME).SceneDelegate
33 | UISceneStoryboardFile
34 | Main
35 |
36 |
37 |
38 |
39 | LSApplicationQueriesSchemes
40 |
41 | share2archivetoday
42 |
43 | NSUserActivityTypes
44 |
45 | org.Gnosco.Share-2-Archive-Today.openURL
46 |
47 | com.apple.security.application-groups
48 |
49 | group.org.Gnosco.Share-2-Archive-Today
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Share-2-Archive-Today
4 | //
5 | import UIKit
6 | import SafariServices
7 | import os.log
8 |
9 | /// Manages the app's window scene and handles URL-based interactions
10 | /// This delegate is responsible for managing the app's window scene lifecycle and URL-based interactions,
11 | /// including handling URLs opened from the share extension
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 | /// The window instance for the app when not using scenes
14 | var window: UIWindow?
15 |
16 | /// Logger for debugging
17 | private let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "SceneDelegate")
18 |
19 | /// Archive URL service
20 | private let archiveService = ArchiveURLService.shared
21 |
22 | // MARK: - URL Handling Methods
23 |
24 | /// Handles URLs when the app is opened via a URL scheme
25 | /// - Parameters:
26 | /// - scene: The UIScene instance that received the URL
27 | /// - URLContexts: A set of URL contexts containing the URLs to handle
28 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
29 | guard let urlContext = URLContexts.first else {
30 | logger.error("No URL contexts provided")
31 | return
32 | }
33 |
34 | // Get the root view controller for the window scene
35 | guard let windowScene = window?.windowScene,
36 | let rootViewController = windowScene.windows.first?.rootViewController else {
37 | logger.error("Could not get root view controller")
38 | return
39 | }
40 |
41 | // Hand off to the shared service
42 | archiveService.handleURLContext(urlContext, from: rootViewController)
43 | }
44 |
45 | /// Called when the scene becomes active to check for pending URLs
46 | /// - Parameter scene: The scene that became active
47 | func sceneDidBecomeActive(_ scene: UIScene) {
48 | logger.debug("Scene became active")
49 |
50 | // Get the root view controller for the window scene
51 | guard let windowScene = window?.windowScene,
52 | let rootViewController = windowScene.windows.first?.rootViewController else {
53 | logger.error("Could not get root view controller")
54 | return
55 | }
56 |
57 | // Check for pending URLs
58 | archiveService.checkForPendingURL(from: rootViewController)
59 | }
60 |
61 | /// Handles continuation of user activity, typically from Universal Links or Handoff
62 | /// - Parameters:
63 | /// - scene: The UIScene instance that will continue the activity
64 | /// - userActivity: The user activity to continue
65 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
66 | if let url = userActivity.webpageURL {
67 | logger.info("Continuing user activity with URL: \(url)")
68 |
69 | // Get the root view controller for the window scene
70 | guard let windowScene = window?.windowScene,
71 | let rootViewController = windowScene.windows.first?.rootViewController else {
72 | logger.error("Could not get root view controller")
73 | return
74 | }
75 |
76 | // Process the URL
77 | processURL(from: url, viewController: rootViewController)
78 | }
79 | }
80 |
81 | /// Processes a URL and opens it in archive.today
82 | /// - Parameters:
83 | /// - url: The URL to process
84 | /// - viewController: The view controller to present from
85 | private func processURL(from url: URL, viewController: UIViewController) {
86 | var finalUrlString: String? = nil
87 |
88 | if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
89 | let urlQueryItem = components.queryItems?.first(where: { $0.name == "url" }) {
90 | finalUrlString = urlQueryItem.value
91 | logger.debug("Extracted URL from query parameters: \(urlQueryItem.value ?? "nil")")
92 | } else {
93 | // Direct URL case
94 | finalUrlString = url.absoluteString
95 | logger.debug("Using direct URL: \(url.absoluteString)")
96 | }
97 |
98 | guard let urlString = finalUrlString else {
99 | logger.error("Could not extract URL string")
100 | return
101 | }
102 |
103 | // Use the archive service to process and archive the URL
104 | archiveService.processAndArchiveURL(urlString, from: viewController)
105 | }
106 |
107 | /// Called when the scene is being released by the system
108 | func sceneDidDisconnect(_ scene: UIScene) {
109 | logger.debug("Scene disconnected")
110 | }
111 |
112 | /// Called when the scene is about to move from an active state to an inactive state
113 | func sceneWillResignActive(_ scene: UIScene) {
114 | logger.debug("Scene will resign active")
115 | }
116 |
117 | /// Called as the scene transitions from the background to the foreground
118 | func sceneWillEnterForeground(_ scene: UIScene) {
119 | logger.debug("Scene will enter foreground")
120 | }
121 |
122 | /// Called as the scene transitions from the foreground to the background
123 | func sceneDidEnterBackground(_ scene: UIScene) {
124 | logger.debug("Scene did enter background")
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/Share-2-Archive-Today.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.org.Gnosco.Share-2-Archive-Today
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/URLProcessor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 | import os.log
4 |
5 | /// A utility class for processing and cleaning URLs before archiving
6 | class URLProcessor {
7 |
8 | /// Logger for debugging
9 | private static let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "URLProcessor")
10 |
11 | /// Set of known tracking parameters that should be removed from URLs
12 | private static let trackingParams: Set = [
13 | "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term",
14 | "fbclid", "gclid", "dclid", "gbraid", "wbraid", "msclkid", "tclid",
15 | "aff_id", "affiliate_id", "ref", "referer", "campaign_id", "ad_id",
16 | "adgroup_id", "adset_id", "creativetype", "placement", "network",
17 | "mc_eid", "mc_cid", "si", "icid", "_ga", "_gid", "scid", "click_id",
18 | "trk", "track", "trk_sid", "sid", "mibextid", "fb_action_ids",
19 | "fb_action_types", "twclid", "igshid", "s_kwcid", "sxsrf", "sca_esv",
20 | "source", "tbo", "sa", "ved", "pi", "fbs", "fbc", "fb_ref", "client", "ei",
21 | "gs_lp", "sclient", "oq", "uact", "bih", "biw"
22 | ]
23 |
24 | /// Set of YouTube-specific parameters that should be removed
25 | private static let unwantedYoutubeParams: Set = [
26 | "feature"
27 | ]
28 |
29 | /// Processes a URL string before archiving
30 | /// - Parameter urlString: Original URL string to process
31 | /// - Returns: Processed URL string with tracking parameters removed and site-specific modifications applied
32 | static func processURL(_ urlString: String) -> String {
33 | // First process any archive.today URLs to extract the original URL
34 | let processedUrl = processArchiveURL(urlString)
35 |
36 | // Then clean tracking parameters and apply site-specific modifications
37 | return cleanTrackingParamsFromURL(processedUrl)
38 | }
39 |
40 | /// Extracts the original URL from an archive.today URL if applicable
41 | /// - Parameter urlString: The URL string which might be an archive URL
42 | /// - Returns: The original URL if it was an archive URL, otherwise the original string
43 | static func processArchiveURL(_ urlString: String) -> String {
44 | guard let url = URL(string: urlString) else {
45 | return "" //If unable to processURL, reutrn nothing
46 | }
47 |
48 | // Check if this is an archive.today URL
49 | if let host = url.host,
50 | host.contains("archive.") {
51 | // Try to extract the original URL using regex
52 | let pattern = "archive\\.[a-z]+/o/[a-zA-Z0-9]+/(.+)"
53 | guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
54 | return urlString
55 | }
56 |
57 | let range = NSRange(urlString.startIndex.. String {
72 | guard let url = URL(string: urlString),
73 | var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
74 | return urlString
75 | }
76 |
77 | // If there are no query parameters, handle special cases then return
78 | if components.queryItems == nil || components.queryItems!.isEmpty {
79 | // Special case for Substack - add no_cover=true
80 | if let host = components.host, host == "substack.com" || host.hasSuffix(".substack.com") {
81 | components.queryItems = [URLQueryItem(name: "no_cover", value: "true")]
82 | }
83 |
84 | // Special case for YouTube - convert shorts to standard format
85 | if let host = components.host, host == "youtube.com" || host.hasSuffix(".youtube.com"),
86 | components.path.contains("/shorts/") {
87 | components.path = components.path.replacingOccurrences(of: "/shorts/", with: "/v/")
88 | }
89 |
90 | return components.url?.absoluteString ?? urlString
91 | }
92 |
93 | // Start with a clean slate for query parameters
94 | let originalQueryItems = components.queryItems!
95 | var newQueryItems: [URLQueryItem] = []
96 |
97 | // Handle special cases
98 | if let host = components.host {
99 | // YouTube specific processing
100 | if host == "youtube.com" || host.hasSuffix(".youtube.com") || host == "youtu.be" {
101 | // Remove "music." prefix if present
102 | if host.hasPrefix("music.") {
103 | components.host = host.replacingOccurrences(of: "music.", with: "")
104 | }
105 |
106 | // Convert YouTube shorts to standard format
107 | if components.path.contains("/shorts/") {
108 | components.path = components.path.replacingOccurrences(of: "/shorts/", with: "/v/")
109 | }
110 |
111 | // Handle nested query parameters (specifically for 'q' parameter)
112 | if let qItem = originalQueryItems.first(where: { $0.name == "q" }),
113 | let qValue = qItem.value,
114 | var nestedComponents = URLComponents(string: qValue) {
115 |
116 | // Clean nested URL's parameters
117 | if let nestedQueryItems = nestedComponents.queryItems {
118 | let filteredNestedItems = nestedQueryItems.filter { item in
119 | !trackingParams.contains(item.name)
120 | }
121 | nestedComponents.queryItems = filteredNestedItems.isEmpty ? nil : filteredNestedItems
122 | }
123 |
124 | // Add back the cleaned nested URL
125 | let nestedURLString = nestedComponents.string ?? qValue
126 | newQueryItems.append(URLQueryItem(name: "q", value: nestedURLString))
127 | }
128 |
129 | // Add all non-tracking, non-YouTube unwanted parameters
130 | for item in originalQueryItems {
131 | if item.name != "q" && // Skip 'q' as we've already handled it
132 | !trackingParams.contains(item.name) &&
133 | !unwantedYoutubeParams.contains(item.name) {
134 | newQueryItems.append(item)
135 | }
136 | }
137 | }
138 | // Substack specific processing
139 | else if host == "substack.com" || host.hasSuffix(".substack.com") {
140 | // Make sure no_cover=true is included
141 | let hasCoverParam = originalQueryItems.contains { $0.name == "no_cover" }
142 | if !hasCoverParam {
143 | newQueryItems.append(URLQueryItem(name: "no_cover", value: "true"))
144 | }
145 |
146 | // Add all non-tracking parameters
147 | for item in originalQueryItems {
148 | if !trackingParams.contains(item.name) {
149 | newQueryItems.append(item)
150 | }
151 | }
152 | }
153 | // Default processing for all other domains
154 | else {
155 | // Just remove tracking parameters
156 | for item in originalQueryItems {
157 | if !trackingParams.contains(item.name) {
158 | newQueryItems.append(item)
159 | }
160 | }
161 | }
162 | }
163 |
164 | // Set the new query items
165 | components.queryItems = newQueryItems.isEmpty ? nil : newQueryItems
166 |
167 | return components.url?.absoluteString ?? urlString
168 | }
169 |
170 | /// Extracts a URL from a text string
171 | /// - Parameter text: The text string that might contain a URL
172 | /// - Returns: An extracted URL string or nil if none is found
173 | static func extractURL(from text: String) -> String? {
174 | // First, try NSDataDetector for URLs (most accurate)
175 | let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
176 |
177 | if let detector = detector {
178 | let matches = detector.matches(in: text, range: NSRange(text.startIndex..., in: text))
179 |
180 | if let match = matches.first, let range = Range(match.range, in: text) {
181 | let urlString = String(text[range])
182 | return urlString
183 | }
184 | }
185 |
186 | // Fallback: If NSDataDetector doesn't find a URL, use regex to look for common URL patterns
187 | // This regex looks for URLs with either http/https or www prefixes
188 | let urlPattern = "(https?://(?:www\\.)?|www\\.)[a-zA-Z0-9][a-zA-Z0-9-]*(?:\\.[a-zA-Z0-9][a-zA-Z0-9-]*)+(?:/[^\\s]*)?"
189 |
190 | if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
191 | let range = NSRange(text.startIndex.. String {
225 | var cleanedUrl = url
226 |
227 | // Handle multiple protocols by taking the last valid one
228 | if let lastHttpsIndex = url.lastIndex(of: "https://".last!), url[url.startIndex.. String {
253 | // First encode - converts & to %26, = to %3D, etc.
254 | let firstEncoding = self.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? self
255 |
256 | // Second encode - converts % to %25
257 | let secondEncoding = firstEncoding.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? firstEncoding
258 |
259 | return secondEncoding
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/URLStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLStore.swift
3 | // Share-2-Archive-Today
4 | //
5 | // Created by Gabirel Fair on 2/9/25.
6 | //
7 | import Foundation
8 | import os.log
9 |
10 | /// A singleton class that manages the storage and retrieval of URLs using UserDefaults
11 | /// This class provides thread-safe access to a shared URL storage that persists across app launches
12 | /// Manages the storage and retrieval of archived URLs using UserDefaults with App Groups
13 | class URLStore {
14 | /// Shared instance for singleton access
15 | static let shared = URLStore()
16 |
17 | /// UserDefaults suite for sharing data between app and extension
18 | private let defaults: UserDefaults?
19 |
20 | /// Key for storing URLs in UserDefaults
21 | private let urlStorageKey = "saved_urls"
22 |
23 | /// Group identifier for sharing data between app and extension
24 | private let appGroupIdentifier = "group.org.Gnosco.Share-2-Archive-Today"
25 |
26 | /// Logger for debugging
27 | private let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "URLStore")
28 |
29 | /// Private initializer for singleton pattern
30 | private init() {
31 | defaults = UserDefaults(suiteName: appGroupIdentifier)
32 |
33 | if defaults == nil {
34 | logger.error("Failed to initialize UserDefaults with app group: \(self.appGroupIdentifier)")
35 | } else {
36 | logger.info("URLStore initialized successfully with app group")
37 | }
38 | }
39 |
40 | /// Saves a URL to persistent storage
41 | /// - Parameter urlString: The URL string to save
42 | /// - Returns: Boolean indicating success of the save operation
43 | @discardableResult
44 | func saveURL(_ urlString: String) -> Bool {
45 | guard let defaults = defaults else {
46 | logger.error("Cannot save URL: UserDefaults is nil")
47 | return false
48 | }
49 |
50 | // Add basic URL validation
51 | guard !urlString.isEmpty, URL(string: urlString) != nil else {
52 | logger.warning("Attempted to save invalid URL: \(urlString)")
53 | return false
54 | }
55 |
56 | var savedURLs = getSavedURLs()
57 |
58 | // Avoid duplicates
59 | if !savedURLs.contains(urlString) {
60 | savedURLs.append(urlString)
61 | defaults.set(savedURLs, forKey: urlStorageKey)
62 | logger.info("Saved new URL to store: \(urlString)")
63 | return true
64 | } else {
65 | logger.info("URL already exists in store: \(urlString)")
66 | }
67 |
68 | return false
69 | }
70 |
71 | /// Retrieves all saved URLs from persistent storage
72 | /// - Returns: Array of saved URL strings
73 | func getSavedURLs() -> [String] {
74 | guard let defaults = defaults else {
75 | logger.error("Cannot get saved URLs: UserDefaults is nil")
76 | return []
77 | }
78 |
79 | let urls = defaults.stringArray(forKey: urlStorageKey) ?? []
80 | logger.debug("Retrieved \(urls.count) URLs from store")
81 | return urls
82 | }
83 |
84 | /// Removes a URL from persistent storage
85 | /// - Parameter urlString: The URL string to remove
86 | /// - Returns: Boolean indicating success of the removal operation
87 | @discardableResult
88 | func removeURL(_ urlString: String) -> Bool {
89 | guard let defaults = defaults else {
90 | logger.error("Cannot remove URL: UserDefaults is nil")
91 | return false
92 | }
93 |
94 | var savedURLs = getSavedURLs()
95 | if let index = savedURLs.firstIndex(of: urlString) {
96 | savedURLs.remove(at: index)
97 | defaults.set(savedURLs, forKey: urlStorageKey)
98 | logger.info("Removed URL from store: \(urlString)")
99 | return true
100 | } else {
101 | logger.warning("URL not found for removal: \(urlString)")
102 | }
103 |
104 | return false
105 | }
106 |
107 | /// Removes all saved URLs from persistent storage
108 | func clearAllURLs() {
109 | guard let defaults = defaults else {
110 | logger.error("Cannot clear URLs: UserDefaults is nil")
111 | return
112 | }
113 |
114 | defaults.removeObject(forKey: urlStorageKey)
115 | logger.info("Cleared all URLs from store")
116 | }
117 |
118 | /// Checks if a URL exists in persistent storage
119 | /// - Parameter urlString: The URL string to check
120 | /// - Returns: Boolean indicating if the URL exists
121 | func urlExists(_ urlString: String) -> Bool {
122 | let exists = getSavedURLs().contains(urlString)
123 | logger.debug("URL exists check: \(urlString) - \(exists)")
124 | return exists
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SafariServices
3 |
4 | class ViewController: UIViewController {
5 |
6 | // MARK: - IBOutlets
7 |
8 | /// Table view displaying the list of archived URLs
9 | @IBOutlet private weak var tableView: UITableView!
10 |
11 | /// View displayed when there are no archived URLs
12 | @IBOutlet private weak var emptyStateView: UIView!
13 |
14 | // MARK: - Properties
15 |
16 | /// Array of saved URL strings, displayed in reverse chronological order
17 | private var urls: [String] = []
18 |
19 | private var helpButton: FloatingButton!
20 |
21 | /// Shared instance of URLStore for managing saved URLs
22 | private let urlStore = URLStore.shared
23 |
24 | // MARK: - Lifecycle Methods
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 | setupRefreshControl()
29 | setupHelpButton()
30 |
31 | // Check if we should show the welcome message
32 | checkForFirstLaunch()
33 | }
34 |
35 | override func viewWillAppear(_ animated: Bool) {
36 | super.viewWillAppear(animated)
37 | refreshUrls()
38 | }
39 |
40 | private func setupHelpButton() {
41 | helpButton = FloatingButton()
42 | view.addSubview(helpButton)
43 |
44 | NSLayoutConstraint.activate([
45 | helpButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
46 | helpButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20)
47 | ])
48 |
49 | helpButton.addTarget(self, action: #selector(showHelpFAQ), for: .touchUpInside)
50 | }
51 |
52 | @objc private func showHelpFAQ() {
53 | let faqVC = FAQViewController()
54 | let navController = UINavigationController(rootViewController: faqVC)
55 | navController.modalPresentationStyle = .formSheet
56 | present(navController, animated: true)
57 | }
58 |
59 | /// Checks if this is the first launch and shows welcome overlay if needed
60 | private func checkForFirstLaunch() {
61 | let welcomeShownKey = "org.Gnosco.Share-2-Archive-Today.welcomeShown"
62 | let welcomeShown = UserDefaults.standard.bool(forKey: welcomeShownKey)
63 |
64 | if !welcomeShown {
65 | // Wait for the view to fully load before showing welcome overlay
66 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
67 | self?.showWelcomeMessage()
68 | UserDefaults.standard.set(true, forKey: welcomeShownKey)
69 | }
70 | }
71 | }
72 |
73 | /// Shows the welcome overlay to first-time users
74 | private func showWelcomeMessage() {
75 | let welcomeView = WelcomeOverlayView(frame: .zero)
76 | welcomeView.onDismiss = { [weak self] in
77 | // Refresh URLs after welcome is dismissed to ensure sample URL is shown
78 | self?.refreshUrls()
79 | }
80 | welcomeView.show(in: self.view)
81 | }
82 |
83 | // MARK: - Setup Methods
84 |
85 | /// Sets up the pull-to-refresh control for the table view
86 | private func setupRefreshControl() {
87 | let refreshControl = UIRefreshControl()
88 | refreshControl.addTarget(self, action: #selector(refreshUrls), for: .valueChanged)
89 | tableView.refreshControl = refreshControl
90 | }
91 |
92 | // MARK: - URL Management Methods
93 |
94 | /// Opens a URL in Archive.today service
95 | /// - Parameter urlString: The URL to archive
96 | private func openInArchiveToday(_ urlString: String) {
97 | // Create URL components for the archive.today service
98 | guard var components = URLComponents(string: "https://archive.today/") else {
99 | showError(message: "Could not create archive URL")
100 | return
101 | }
102 |
103 | // Add query items - this properly encodes the URL as a parameter
104 | components.queryItems = [
105 | URLQueryItem(name: "run", value: "1"),
106 | URLQueryItem(name: "url", value: urlString)
107 | ]
108 |
109 | // Get the final URL
110 | guard let archiveUrl = components.url else {
111 | showError(message: "Could not create archive URL")
112 | return
113 | }
114 |
115 | let safariVC = SFSafariViewController(url: archiveUrl)
116 | safariVC.preferredControlTintColor = .systemBlue
117 | present(safariVC, animated: true)
118 | }
119 |
120 | /// Opens the original URL in Safari
121 | /// - Parameter urlString: The URL to open
122 | private func openOriginalUrl(_ urlString: String) {
123 | // No need to process the URL again - it was processed when saved to URLStore
124 | guard let url = URL(string: urlString) else {
125 | showError(message: "Invalid URL")
126 | return
127 | }
128 |
129 | let safariVC = SFSafariViewController(url: url)
130 | safariVC.preferredControlTintColor = .systemBlue
131 | present(safariVC, animated: true)
132 | }
133 |
134 | // MARK: - UI Update Methods
135 |
136 | /// Updates the UI based on whether there are any URLs
137 | private func updateEmptyState() {
138 | emptyStateView.isHidden = !urls.isEmpty
139 | tableView.isHidden = urls.isEmpty
140 | }
141 |
142 | // MARK: - IBActions
143 |
144 | /// Handles the Clear All button tap
145 | /// - Parameter sender: The button that triggered the action
146 | @IBAction func clearAllTapped(_ sender: Any) {
147 | let alert = UIAlertController(
148 | title: "Clear All URLs",
149 | message: "Are you sure you want to delete all saved URLs? This action cannot be undone.",
150 | preferredStyle: .alert
151 | )
152 |
153 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
154 | alert.addAction(UIAlertAction(title: "Clear All", style: .destructive) { [weak self] _ in
155 | self?.urlStore.clearAllURLs()
156 | self?.refreshUrls()
157 | })
158 |
159 | present(alert, animated: true)
160 | }
161 |
162 | /// Handles the Edit button tap
163 | /// - Parameter sender: The button that triggered the action
164 | @IBAction func editButtonTapped(_ sender: Any) {
165 | tableView.setEditing(!tableView.isEditing, animated: true)
166 | }
167 |
168 | /// Refreshes the list of URLs from storage
169 | @objc private func refreshUrls() {
170 | urls = urlStore.getSavedURLs().reversed()
171 | updateEmptyState()
172 | tableView.reloadData()
173 | tableView.refreshControl?.endRefreshing()
174 | }
175 |
176 | /// Shows an error alert to the user
177 | /// - Parameter message: The error message to display
178 | private func showError(message: String) {
179 | let alert = UIAlertController(
180 | title: "Error",
181 | message: message,
182 | preferredStyle: .alert
183 | )
184 | alert.addAction(UIAlertAction(title: "OK", style: .default))
185 | present(alert, animated: true)
186 | }
187 | }
188 |
189 | // MARK: - UITableViewDataSource
190 |
191 | extension ViewController: UITableViewDataSource {
192 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
193 | return urls.count
194 | }
195 |
196 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
197 | let cell = tableView.dequeueReusableCell(withIdentifier: "URLCell", for: indexPath)
198 | cell.textLabel?.text = urls[indexPath.row]
199 | cell.textLabel?.numberOfLines = 0
200 | cell.accessoryType = .disclosureIndicator
201 | return cell
202 | }
203 |
204 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
205 | if editingStyle == .delete {
206 | let urlToDelete = urls[indexPath.row]
207 | urlStore.removeURL(urlToDelete)
208 | urls.remove(at: indexPath.row)
209 | tableView.deleteRows(at: [indexPath], with: .fade)
210 | updateEmptyState()
211 | }
212 | }
213 | }
214 |
215 | // MARK: - UITableViewDelegate
216 |
217 | extension ViewController: UITableViewDelegate {
218 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
219 | tableView.deselectRow(at: indexPath, animated: true)
220 |
221 | let url = urls[indexPath.row]
222 | let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
223 |
224 | // View in Archive.today
225 | alert.addAction(UIAlertAction(title: "View in Archive.today", style: .default) { [weak self] _ in
226 | self?.openInArchiveToday(url)
227 | })
228 |
229 | // Open Original URL
230 | alert.addAction(UIAlertAction(title: "Open Cleaned Original URL", style: .default) { [weak self] _ in
231 | self?.openOriginalUrl(url)
232 | })
233 |
234 | // Copy URL
235 | alert.addAction(UIAlertAction(title: "Copy Cleaned URL", style: .default) { _ in
236 | UIPasteboard.general.string = url
237 | })
238 |
239 | // Delete
240 | alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
241 | guard let self = self else { return }
242 | self.tableView(tableView, commit: .delete, forRowAt: indexPath)
243 | })
244 |
245 | // Cancel
246 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
247 |
248 | // iPad support
249 | if let popoverController = alert.popoverPresentationController {
250 | if let cell = tableView.cellForRow(at: indexPath) {
251 | popoverController.sourceView = cell
252 | popoverController.sourceRect = cell.bounds
253 | }
254 | }
255 |
256 | present(alert, animated: true)
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/Share-2-Archive-Today/WelcomeOverlayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WelcomeOverlayView.swift
3 | // Share-2-Archive-Today
4 | import UIKit
5 |
6 | class WelcomeOverlayView: UIView {
7 |
8 | // MARK: - UI Elements
9 |
10 | private let containerView: UIView = {
11 | let view = UIView()
12 | view.backgroundColor = .systemBackground
13 | view.layer.cornerRadius = 16
14 | view.layer.shadowColor = UIColor.black.cgColor
15 | view.layer.shadowOpacity = 0.2
16 | view.layer.shadowOffset = CGSize(width: 0, height: 2)
17 | view.layer.shadowRadius = 8
18 | view.translatesAutoresizingMaskIntoConstraints = false
19 | return view
20 | }()
21 |
22 | private let titleLabel: UILabel = {
23 | let label = UILabel()
24 | label.text = "Together we can preserve history!"
25 | label.font = .systemFont(ofSize: 22, weight: .bold)
26 | label.textAlignment = .center
27 | label.numberOfLines = 0
28 | label.translatesAutoresizingMaskIntoConstraints = false
29 | return label
30 | }()
31 |
32 | private let imageView: UIImageView = {
33 | let imageView = UIImageView()
34 | imageView.contentMode = .scaleAspectFit
35 | imageView.tintColor = .systemBlue
36 | imageView.image = UIImage(systemName: "archivebox.fill")
37 | imageView.translatesAutoresizingMaskIntoConstraints = false
38 | return imageView
39 | }()
40 |
41 | private let descriptionLabel: UILabel = {
42 | let label = UILabel()
43 | label.text = "This app helps you save web pages using archive.today for future reference.\n\nUse the share extension in Safari or other apps to quickly archive links you want to preserve.\nPlease note archived websites CAN NOT be deleted."
44 | label.font = .systemFont(ofSize: 16)
45 | label.textColor = .secondaryLabel
46 | label.textAlignment = .center
47 | label.numberOfLines = 0
48 | label.translatesAutoresizingMaskIntoConstraints = false
49 | return label
50 | }()
51 |
52 | private let getStartedButton: UIButton = {
53 | let button = UIButton(type: .system)
54 | button.setTitle("Get Started", for: .normal)
55 | button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
56 | button.backgroundColor = .systemBlue
57 | button.setTitleColor(.white, for: .normal)
58 | button.layer.cornerRadius = 12
59 | button.translatesAutoresizingMaskIntoConstraints = false
60 | return button
61 | }()
62 |
63 | // MARK: - Properties
64 |
65 | var onDismiss: (() -> Void)?
66 |
67 | // MARK: - Initialization
68 |
69 | override init(frame: CGRect) {
70 | super.init(frame: frame)
71 | setupView()
72 | }
73 |
74 | required init?(coder: NSCoder) {
75 | super.init(coder: coder)
76 | setupView()
77 | }
78 |
79 | // MARK: - Setup
80 |
81 | private func setupView() {
82 | backgroundColor = UIColor.black.withAlphaComponent(0.4)
83 |
84 | addSubview(containerView)
85 | containerView.addSubview(titleLabel)
86 | containerView.addSubview(imageView)
87 | containerView.addSubview(descriptionLabel)
88 | containerView.addSubview(getStartedButton)
89 |
90 | NSLayoutConstraint.activate([
91 | // Container constraints
92 | containerView.centerXAnchor.constraint(equalTo: centerXAnchor),
93 | containerView.centerYAnchor.constraint(equalTo: centerYAnchor),
94 | containerView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.85),
95 | containerView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor, multiplier: 0.7),
96 |
97 | // Image constraints
98 | imageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 30),
99 | imageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
100 | imageView.heightAnchor.constraint(equalToConstant: 80),
101 | imageView.widthAnchor.constraint(equalToConstant: 80),
102 |
103 | // Title constraints
104 | titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20),
105 | titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),
106 | titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
107 |
108 | // Description constraints
109 | descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
110 | descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),
111 | descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
112 |
113 | // Button constraints
114 | getStartedButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 30),
115 | getStartedButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),
116 | getStartedButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
117 | getStartedButton.heightAnchor.constraint(equalToConstant: 50),
118 | getStartedButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -30)
119 | ])
120 |
121 | // Add tap gesture to button
122 | getStartedButton.addTarget(self, action: #selector(dismissView), for: .touchUpInside)
123 | }
124 |
125 | // MARK: - Actions
126 |
127 | @objc private func dismissView() {
128 | animate(isShowing: false) { [weak self] in
129 | self?.onDismiss?()
130 | self?.removeFromSuperview()
131 | }
132 | }
133 |
134 | // MARK: - Animation
135 |
136 | func show(in parentView: UIView, completion: (() -> Void)? = nil) {
137 | parentView.addSubview(self)
138 | self.frame = parentView.bounds
139 |
140 | // Start with container scaled down
141 | containerView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
142 | containerView.alpha = 0
143 |
144 | animate(isShowing: true, completion: completion)
145 | }
146 |
147 | private func animate(isShowing: Bool, completion: (() -> Void)? = nil) {
148 | let transform: CGAffineTransform = isShowing ? .identity : CGAffineTransform(scaleX: 0.8, y: 0.8)
149 | let alpha: CGFloat = isShowing ? 1.0 : 0.0
150 |
151 | UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
152 | self.containerView.transform = transform
153 | self.containerView.alpha = alpha
154 | self.backgroundColor = isShowing ? UIColor.black.withAlphaComponent(0.4) : UIColor.clear
155 | }, completion: { _ in
156 | completion?()
157 | })
158 | }
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/Base.lproj/MainInterface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
40 |
46 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionAttributes
8 |
9 |
10 | NSExtensionActivationSupportsWebURLWithMaxCount
11 | 1
12 |
13 |
14 | NSExtensionActivationSupportsImageWithMaxCount
15 | 1
16 |
17 |
18 | NSExtensionActivationSupportsText
19 |
20 |
21 |
22 | NSExtensionActivationSupportsWebPageWithMaxCount
23 | 1
24 |
25 |
26 | NSExtensionActivationSupportsHTMLWithMaxCount
27 | 1
28 |
29 |
30 | NSExtensionActivationSupportsFileURLWithMaxCount
31 | 1
32 |
33 |
34 | NSExtensionActivationDictionaryVersion
35 | 2
36 |
37 |
38 | NSExtensionActivationSupportsSafariWebPageWithMaxCount
39 | 1
40 |
41 | NSExtensionActivationRule
42 |
43 | SUBQUERY (
44 | extensionItems,
45 | $extensionItem,
46 | SUBQUERY (
47 | $extensionItem.attachments,
48 | $attachment,
49 | ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data"
50 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url"
51 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text"
52 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.html"
53 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
54 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
55 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.safari.bookmark"
56 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.webarchive"
57 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass"
58 | ).@count >= 1
59 | ).@count == 1
60 | OR (
61 | SUBQUERY (
62 | extensionItems,
63 | $extensionItem,
64 | SUBQUERY (
65 | $extensionItem.attachments,
66 | $attachment,
67 | ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data"
68 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url"
69 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text"
70 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.html"
71 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
72 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.safari.bookmark"
73 | OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass"
74 | ).@count >= 1
75 | ).@count == 2
76 | AND SUBQUERY (
77 | extensionItems,
78 | $extensionItem,
79 | SUBQUERY (
80 | $extensionItem.attachments,
81 | $attachment,
82 | ANY $attachment.registeredTypeIdentifiers UTI-EQUALS "public.url"
83 | OR ANY $attachment.registeredTypeIdentifiers UTI-EQUALS "public.image"
84 | ).@count >= 1
85 | ).@count == 1
86 | )
87 |
88 |
89 | NSExtensionMainStoryboard
90 | MainInterface
91 | NSExtensionPointIdentifier
92 | com.apple.share-services
93 |
94 | com.apple.security.application-groups
95 |
96 | group.org.Gnosco.Share-2-Archive-Today
97 |
98 | NSUserActivityTypes
99 |
100 | NSUserActivityTypeBrowsingWeb
101 | org.Gnosco.Share-2-Archive-Today.openURL
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyTrackingDomains
8 |
9 | NSPrivacyAccessedAPITypes
10 |
11 |
12 | NSPrivacyAccessedAPIType
13 | NSPrivacyAccessedAPICategoryFileTimestamp
14 | NSPrivacyAccessedAPITypeReasons
15 |
16 | C617.1
17 | 3B52.1
18 |
19 |
20 |
21 | NSPrivacyAccessedAPIType
22 | NSPrivacyAccessedAPICategoryUserDefaults
23 | NSPrivacyAccessedAPITypeReasons
24 |
25 | CA92.1
26 | 1C8F.1
27 |
28 |
29 |
30 | NSPrivacyCollectedDataTypes
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/QRCodeScanner.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Vision
3 | import os.log
4 | import CoreServices
5 |
6 | /// Utility class for scanning QR codes from images
7 | class QRCodeScanner {
8 |
9 | /// Logger for debugging
10 | private static let logger = Logger(subsystem: "org.Gnosco.Share-2-Archive-Today", category: "QRCodeScanner")
11 |
12 | /// Scans an image for QR codes and extracts URLs
13 | /// - Parameters:
14 | /// - image: The image to scan for QR codes
15 | /// - completion: Completion handler that returns an optional URL string if found,
16 | /// or an array of URLs if multiple QR codes are detected
17 | static func scanQRCode(from image: UIImage, completion: @escaping (String?) -> Void) {
18 | scanQRCodeMultiple(from: image) { urls in
19 | // For backward compatibility, return the first URL found
20 | completion(urls.first)
21 | }
22 | }
23 |
24 | /// Scans an image for multiple QR codes and extracts all URLs
25 | /// - Parameters:
26 | /// - image: The image to scan for QR codes
27 | /// - completion: Completion handler that returns an array of URL strings
28 | static func scanQRCodeMultiple(from image: UIImage, completion: @escaping ([String]) -> Void) {
29 | guard let cgImage = image.cgImage else {
30 | logger.error("Failed to get CGImage from UIImage")
31 | completion([])
32 | return
33 | }
34 |
35 | // Create a higher quality image if needed
36 | var imageToProcess = cgImage
37 | // If image is small, try to scale it up for better QR code detection
38 | if cgImage.width < 300 || cgImage.height < 300 {
39 | if let scaledImage = scaleUpImage(image, toWidth: 600)?.cgImage {
40 | imageToProcess = scaledImage
41 | logger.info("Scaled up small image for better QR detection")
42 | }
43 | }
44 |
45 | let request = VNDetectBarcodesRequest { request, error in
46 | if let error = error {
47 | logger.error("QR code scanning error: \(error.localizedDescription)")
48 | completion([])
49 | return
50 | }
51 |
52 | guard let results = request.results as? [VNBarcodeObservation], !results.isEmpty else {
53 | logger.warning("No barcode results found")
54 | completion([])
55 | return
56 | }
57 |
58 | logger.info("Found \(results.count) barcode results")
59 | var detectedUrls: [String] = []
60 |
61 | // Process all detected QR codes
62 | for result in results {
63 | logger.debug("Found barcode of type: \(result.symbology.rawValue) with confidence: \(result.confidence)")
64 |
65 | // Filter for QR code types with good confidence
66 | if result.symbology == .qr && result.confidence > 0.9 {
67 | if let payloadString = result.payloadStringValue {
68 | logger.info("Found QR code with payload: \(payloadString)")
69 |
70 | // Try multiple approaches to extract a URL
71 | if let url = extractURLFromQRPayload(payloadString) {
72 | if !detectedUrls.contains(url) {
73 | logger.info("Extracted URL from QR code: \(url)")
74 | detectedUrls.append(url)
75 | }
76 | }
77 | }
78 | }
79 | }
80 |
81 | // If we found no URLs, try again with a lower confidence threshold
82 | if detectedUrls.isEmpty {
83 | logger.info("Retrying with lower confidence threshold")
84 | for result in results where result.symbology == .qr {
85 | if let payloadString = result.payloadStringValue {
86 | if let url = extractURLFromQRPayload(payloadString) {
87 | logger.info("Extracted URL with lower confidence: \(url)")
88 | detectedUrls.append(url)
89 | break // Just get the first one in this case
90 | }
91 | }
92 | }
93 | }
94 |
95 | // Return all detected URLs
96 | completion(detectedUrls)
97 | }
98 |
99 | // Set accuracy level based on iOS version
100 | setOptimalRevision(for: request)
101 |
102 | let handler = VNImageRequestHandler(cgImage: imageToProcess, orientation: transformImageOrientation(image.imageOrientation), options: [:])
103 |
104 | do {
105 | try handler.perform([request])
106 | } catch {
107 | logger.error("Failed to perform QR code detection: \(error.localizedDescription)")
108 | completion([])
109 | }
110 | }
111 |
112 | /// Extracts a URL from a QR code payload string using multiple methods
113 | /// - Parameter payload: String data from the QR code
114 | /// - Returns: URL string if one was found
115 | private static func extractURLFromQRPayload(_ payload: String) -> String? {
116 | // Method 1: Direct URL extraction using URLProcessor
117 | if let url = URLProcessor.extractURL(from: payload) {
118 | return url
119 | }
120 |
121 | // Method 2: Check if the payload is already a valid URL
122 | if let url = URL(string: payload), url.scheme != nil {
123 | return payload
124 | }
125 |
126 | // Method 3: Look for URL patterns in the payload
127 | let urlDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
128 | if let detector = urlDetector {
129 | let matches = detector.matches(in: payload, range: NSRange(payload.startIndex..., in: payload))
130 | if let match = matches.first, let range = Range(match.range, in: payload) {
131 | let urlString = String(payload[range])
132 | return urlString
133 | }
134 | }
135 |
136 | // Method 4: Check for common URL formats without protocol
137 | let commonDomains = ["com", "org", "net", "io", "dev", "co", "app"]
138 | let words = payload.components(separatedBy: .whitespacesAndNewlines)
139 | for word in words {
140 | // Look for potential domain names
141 | if word.contains(".") {
142 | for domain in commonDomains {
143 | if word.hasSuffix(".\(domain)") || word.contains(".\(domain)/") {
144 | // Likely a domain without http/https
145 | return "https://\(word)"
146 | }
147 | }
148 | }
149 | }
150 |
151 | // If no URL patterns found, return nil
152 | return nil
153 | }
154 |
155 | /// Scales up an image for better QR code detection
156 | /// - Parameters:
157 | /// - image: Original image
158 | /// - width: Target width
159 | /// - Returns: Scaled image
160 | private static func scaleUpImage(_ image: UIImage, toWidth width: CGFloat) -> UIImage? {
161 | let scale = width / image.size.width
162 | let newHeight = image.size.height * scale
163 |
164 | UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: newHeight), false, 0)
165 | image.draw(in: CGRect(x: 0, y: 0, width: width, height: newHeight))
166 | let newImage = UIGraphicsGetImageFromCurrentImageContext()
167 | UIGraphicsEndImageContext()
168 |
169 | return newImage
170 | }
171 |
172 | /// Transforms UIImage orientation to CGImagePropertyOrientation
173 | /// - Parameter orientation: UIImage orientation
174 | /// - Returns: CGImagePropertyOrientation value
175 | private static func transformImageOrientation(_ orientation: UIImage.Orientation) -> CGImagePropertyOrientation {
176 | switch orientation {
177 | case .up: return .up
178 | case .down: return .down
179 | case .left: return .left
180 | case .right: return .right
181 | case .upMirrored: return .upMirrored
182 | case .downMirrored: return .downMirrored
183 | case .leftMirrored: return .leftMirrored
184 | case .rightMirrored: return .rightMirrored
185 | @unknown default: return .up
186 | }
187 | }
188 |
189 | /// Sets the optimal revision for barcode detection based on iOS version
190 | /// - Parameter request: The VNDetectBarcodesRequest to configure
191 | private static func setOptimalRevision(for request: VNDetectBarcodesRequest) {
192 | if #available(iOS 17.0, *){
193 | request.revision = VNDetectBarcodesRequestRevision4
194 | logger.debug("Using VNDetectBarcodesRequestRevision3 (iOS 17+)")
195 | } else if #available(iOS 16.0, *) {
196 | // Use the newest revision for iOS 16+
197 | request.revision = VNDetectBarcodesRequestRevision3
198 | logger.debug("Using VNDetectBarcodesRequestRevision3 (iOS 16+)")
199 | } else if #available(iOS 15.0, *) {
200 | // Use revision 2 for iOS 15
201 | request.revision = VNDetectBarcodesRequestRevision2
202 | logger.debug("Using VNDetectBarcodesRequestRevision2 (iOS 15)")
203 | } else {
204 | // For earlier iOS versions, no specific revision is set (uses default)
205 | logger.debug("Using default VNDetectBarcodesRequest revision (iOS < 15)")
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/Toast.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// A utility class for displaying toast messages
4 | class Toast {
5 |
6 | /// Shows a toast message in the specified view
7 | /// - Parameters:
8 | /// - message: The message to display
9 | /// - view: The view to display the toast in
10 | /// - duration: How long the toast should be displayed (in seconds)
11 | static func show(message: String, in view: UIView, duration: TimeInterval = 2.0) {
12 | let toastContainer = UIView()
13 | toastContainer.backgroundColor = UIColor.black.withAlphaComponent(0.7)
14 | toastContainer.layer.cornerRadius = 16
15 | toastContainer.clipsToBounds = true
16 | toastContainer.translatesAutoresizingMaskIntoConstraints = false
17 |
18 | let toastLabel = UILabel()
19 | toastLabel.textColor = UIColor.white
20 | toastLabel.textAlignment = .center
21 | toastLabel.font = UIFont.systemFont(ofSize: 14)
22 | toastLabel.text = message
23 | toastLabel.clipsToBounds = true
24 | toastLabel.numberOfLines = 0
25 | toastLabel.translatesAutoresizingMaskIntoConstraints = false
26 |
27 | toastContainer.addSubview(toastLabel)
28 | view.addSubview(toastContainer)
29 |
30 | NSLayoutConstraint.activate([
31 | toastLabel.leadingAnchor.constraint(equalTo: toastContainer.leadingAnchor, constant: 16),
32 | toastLabel.trailingAnchor.constraint(equalTo: toastContainer.trailingAnchor, constant: -16),
33 | toastLabel.topAnchor.constraint(equalTo: toastContainer.topAnchor, constant: 10),
34 | toastLabel.bottomAnchor.constraint(equalTo: toastContainer.bottomAnchor, constant: -10),
35 |
36 | toastContainer.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 20),
37 | toastContainer.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -20),
38 | toastContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
39 | toastContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
40 | ])
41 |
42 | toastContainer.alpha = 0.0
43 |
44 | UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
45 | toastContainer.alpha = 1.0
46 | }, completion: { _ in
47 | UIView.animate(withDuration: 0.2, delay: duration, options: .curveEaseOut, animations: {
48 | toastContainer.alpha = 0.0
49 | }, completion: { _ in
50 | toastContainer.removeFromSuperview()
51 | })
52 | })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/URLProcessingManager.swift:
--------------------------------------------------------------------------------
1 | // URLProcessingManager.swift
2 |
3 | import Foundation
4 |
5 | /// Singleton to manage URL processing state across the share extension
6 | class URLProcessingManager {
7 | /// Shared instance of the manager
8 | static let shared = URLProcessingManager()
9 |
10 | /// Flag indicating whether a URL has been found
11 | private var _urlFound = false
12 |
13 | /// Thread-safe access to urlFound flag
14 | var urlFound: Bool {
15 | get {
16 | // Using objc_sync_enter/exit for thread safety
17 | objc_sync_enter(self)
18 | defer { objc_sync_exit(self) }
19 | return _urlFound
20 | }
21 | set {
22 | objc_sync_enter(self)
23 | _urlFound = newValue
24 | objc_sync_exit(self)
25 | }
26 | }
27 |
28 | /// Reset the state of the manager
29 | func reset() {
30 | urlFound = false
31 | }
32 |
33 | /// Private initializer to enforce singleton pattern
34 | private init() {}
35 | }
36 |
--------------------------------------------------------------------------------
/ios/Share-2-Archive-Today/URLShareExtension/URLShareExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.org.Gnosco.Share-2-Archive-Today
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/privacy.policy:
--------------------------------------------------------------------------------
1 | This app does not use, collect, or store any user data, or Personal Identifying Information (PII), or diagnostic data, or debugging data about you or your phone.
2 | Seriously, this app just adds a tool to your share menu to open the archived version of a URL.
3 |
--------------------------------------------------------------------------------