Compare commits

...

28 Commits

Author SHA1 Message Date
7747aa6b0e Merge branch 'master' into jf/android-build-fix 2025-06-29 18:44:36 -07:00
fda0b8ede5 fix: android min sdk version. (#523) 2025-06-29 18:42:38 -07:00
bb51b3b0cb update podfile.lock. 2025-06-29 17:58:05 -07:00
3ff8e82966 fix: android min sdk version. 2025-06-29 17:47:23 -07:00
1dc1cce12b fix: iOS share extension. (#522) 2025-06-29 17:26:00 -07:00
1c87d741cb fix: android build issue. (#521) 2025-06-28 23:29:00 -07:00
d3b01b97fd fix: offline stories limit. (#520) 2025-06-13 01:36:22 -07:00
f0413f99f0 feat: allow limiting how many stories to be downloaded. (#519) 2025-06-12 23:22:03 -07:00
2c213dee58 chore: upgrade to flutter 3.32.4 (#518) 2025-06-12 21:39:48 -07:00
1652de4c2d fix: ignore network error in parser verifier. (#517) 2025-06-12 21:36:29 -07:00
df807a4a11 feat: improve back gesture on comment details screen. (#516) 2025-06-12 21:11:12 -07:00
4f7a515490 fix: theming. (#508) 2025-01-25 12:46:50 -08:00
341e04d645 fix: slidable action icon color. (#506) 2025-01-21 23:10:42 -08:00
872c4359d4 fix: item page deep link route. (#505) 2025-01-20 21:55:05 -08:00
de1eac31da fix: item screen deeplink. (#504) 2025-01-20 21:37:25 -08:00
0dab102904 chore: update publish_ios.yml (#502) 2025-01-20 19:40:51 -08:00
9c616eb734 chore: update publish_ios.yml (#501) 2025-01-20 14:13:12 -08:00
691a0cb2ac chore: bump Xcode version. (#500) 2025-01-20 13:41:30 -08:00
6612227249 chore: bump macOS version. (#499) 2025-01-20 09:07:13 -08:00
78e022f3cb chore: update .ruby-version (#498) 2025-01-20 02:45:45 -08:00
677b9d4b7d chore: bump macOS version. (#497) 2025-01-20 02:23:14 -08:00
cc55913022 fix: iOS fastlane. (#496) 2025-01-20 02:06:22 -08:00
08973bb829 fix: widget config. (#495) 2025-01-20 01:36:59 -08:00
fdce94f2e7 feat: home screen widget for iOS. (#494) 2025-01-20 01:12:44 -08:00
0897abf27e chore: bump flutter version to 3.27.2. (#493) 2025-01-19 22:32:03 -08:00
f07254dbd4 fix: web parser. (#489) 2024-11-19 00:02:59 -08:00
1408b7343a chore: bump fastlane version. (#486) 2024-10-23 21:59:53 -07:00
bedc3b66ec fix comment parser. (#485) 2024-10-23 18:03:02 -07:00
94 changed files with 3106 additions and 876 deletions

View File

@ -15,7 +15,7 @@ on:
jobs:
build_and_publish:
runs-on: macos-13
runs-on: macos-15
timeout-minutes: 30
env:
@ -27,7 +27,7 @@ jobs:
- name: Set XCode version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.0'
xcode-version: '16.0'
- name: Check out from git
uses: actions/checkout@v3

1
.gitignore vendored
View File

@ -44,3 +44,4 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android/build/reports

View File

@ -1 +1 @@
2.7.5
3.3.0

View File

@ -31,11 +31,15 @@ if (keystorePropertiesFile.exists()) {
android {
compileSdkVersion 34
namespace "com.jiaqifeng.hacki"
compileSdkVersion 35
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
}
kotlinOptions {
@ -49,7 +53,7 @@ android {
defaultConfig {
applicationId "com.jiaqifeng.hacki"
minSdkVersion 25
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@ -78,7 +82,8 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.20"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]

View File

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
kotlin.jvm.target.validation.mode = IGNORE

View File

@ -1,6 +1,7 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

View File

@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
id "com.android.application" version "8.11.0" apply false
id "org.jetbrains.kotlin.android" version "2.2.0" apply false
}
include ":app"

View File

@ -25,6 +25,8 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "dev.britannio.in_app_review"
compileSdkVersion 31
compileOptions {

View File

@ -1,18 +1,78 @@
//
// ActionViewController.swift
// Action Extension
//
// Created by Jiaqi Feng on 5/22/22.
//
import UIKit
import MobileCoreServices
import UniformTypeIdentifiers
public class SharedMediaFile: Codable {
var path: String
var mimeType: String?
var thumbnail: String? // video thumbnail
var duration: Double? // video duration in milliseconds
var message: String? // post message
var type: SharedMediaType
public init(
path: String,
mimeType: String? = nil,
thumbnail: String? = nil,
duration: Double? = nil,
message: String?=nil,
type: SharedMediaType) {
self.path = path
self.mimeType = mimeType
self.thumbnail = thumbnail
self.duration = duration
self.message = message
self.type = type
}
}
public enum SharedMediaType: String, Codable, CaseIterable {
case image
case video
case text
case file
case url
public var toUTTypeIdentifier: String {
if #available(iOS 14.0, *) {
switch self {
case .image:
return UTType.image.identifier
case .video:
return UTType.movie.identifier
case .text:
return UTType.text.identifier
case .file:
return UTType.fileURL.identifier
case .url:
return UTType.url.identifier
}
}
switch self {
case .image:
return "public.image"
case .video:
return "public.movie"
case .text:
return "public.text"
case .file:
return "public.file-url"
case .url:
return "public.url"
}
}
}
let kSchemePrefix = "ShareMedia"
let kUserDefaultsKey = "ShareKey"
let kUserDefaultsMessageKey = "ShareMessageKey"
let kAppGroupIdKey = "AppGroupId"
class ActionViewController: UIViewController {
let hostAppBundleIdentifier = "com.jiaqi.hacki"
let sharedKey = "ShareKey"
var hostAppBundleIdentifier = "com.jiaqi.hacki"
var appGroupId = "group.com.jiaqi.hacki"
var sharedText: [String] = []
var sharedMedia: [SharedMediaFile] = []
let urlContentType = UTType.url
@IBOutlet weak var imageView: UIImageView!
@ -39,13 +99,11 @@ class ActionViewController: UIViewController {
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.sharedMedia.removeAll()
this.sharedMedia.append(.init(path: item.absoluteString, type: .url))
print(this.sharedText)
this.redirectToHostApp()
this.saveAndRedirect()
}
} else {
self?.dismissWithError()
}
@ -65,25 +123,69 @@ class ActionViewController: UIViewController {
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
// Save shared media and redirect to host app
private func saveAndRedirect(message: String? = nil) {
let userDefaults = UserDefaults(suiteName: appGroupId)
userDefaults?.set(toData(data: sharedMedia), forKey: kUserDefaultsKey)
userDefaults?.set(message, forKey: kUserDefaultsMessageKey)
userDefaults?.synchronize()
redirectToHostApp()
}
private func toData(data: [SharedMediaFile]) -> Data {
let encodedData = try? JSONEncoder().encode(data)
return encodedData!
}
private func redirectToHostApp() {
let url = URL(string: "ShareMedia-\(hostAppBundleIdentifier)://dataUrl=\(sharedKey)#text")
// ids may not loaded yet so we need loadIds here too
loadIds()
let url = URL(string: "\(kSchemePrefix)-\(hostAppBundleIdentifier):share")
var responder = self as UIResponder?
let selectorOpenURL = sel_registerName("openURL:")
while (responder != nil) {
if let application = responder as? UIApplication {
application.performSelector(inBackground: selectorOpenURL, with: url)
if #available(iOS 18.0, *) {
while responder != nil {
if let application = responder as? UIApplication {
application.open(url!, options: [:], completionHandler: nil)
}
responder = responder?.next
}
} else {
let selectorOpenURL = sel_registerName("openURL:")
responder = responder!.next
while (responder != nil) {
if (responder?.responds(to: selectorOpenURL))! {
_ = responder?.perform(selectorOpenURL, with: url)
}
responder = responder!.next
}
}
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func loadIds() {
// loading Share extension App Id
let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!
// extract host app bundle id from ShareExtension id
// by default it's <hostAppBundleIdentifier>.<ShareExtension>
// for example: "com.kasem.sharing.Share-Extension" -> com.kasem.sharing
let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".")
hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[..<lastIndexOfPoint!])
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
// loading custom AppGroupId from Build Settings or use group.<hostAppBundleIdentifier>
let customAppGroupId = Bundle.main.object(forInfoDictionaryKey: kAppGroupIdKey) as? String
appGroupId = customAppGroupId ?? defaultAppGroupId
}
@IBAction func done() {
// Return any edited content to the host app.
// This template doesn't do anything, so we just echo the passed in items.
self.extensionContext!.completeRequest(returningItems: self.extensionContext!.inputItems, completionHandler: nil)
}
}

View File

@ -1,7 +1,9 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
CFPropertyList (3.0.7)
base64
nkf
rexml
activesupport (6.1.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
@ -9,30 +11,31 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.889.0)
aws-sdk-core (3.191.1)
aws-partitions (1.994.0)
aws-sdk-core (3.211.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.169.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
@ -87,7 +90,7 @@ GEM
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.109.0)
faraday (1.10.3)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -108,22 +111,22 @@ GEM
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.0)
fastlane (2.219.0)
fastimage (2.3.1)
fastlane (2.225.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@ -132,6 +135,7 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@ -144,10 +148,10 @@ GEM
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
@ -156,7 +160,9 @@ GEM
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
@ -198,40 +204,42 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.7)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.7.1)
jwt (2.7.1)
mini_magick (4.12.0)
json (2.7.2)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.16.3)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.0)
multipart-post (2.4.1)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
optparse (0.4.0)
nkf (0.2.0)
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (4.0.7)
rake (13.1.0)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rexml (3.3.8)
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
security (0.1.5)
signet (0.18.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@ -240,6 +248,7 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
@ -253,18 +262,16 @@ GEM
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.9.1)
unicode-display_width (2.5.0)
unf (0.2.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
@ -273,6 +280,7 @@ GEM
PLATFORMS
universal-darwin-21
universal-darwin-22
universal-darwin-23
x86_64-darwin-19

View File

@ -1,7 +1,6 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
@ -10,13 +9,13 @@ PODS:
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- OrderedSet (~> 6.0.3)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
@ -25,23 +24,23 @@ PODS:
- integration_test (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- OrderedSet (5.0.0)
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- qr_code_scanner (0.2.0):
- qr_code_scanner_plus (0.2.6):
- Flutter
- MTBBarcodeScanner
- receive_sharing_intent (1.5.3):
- receive_sharing_intent (1.8.1):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- synced_shared_preferences (0.0.1):
@ -52,11 +51,12 @@ PODS:
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- workmanager (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
@ -68,15 +68,15 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
@ -86,7 +86,7 @@ SPEC REPOS:
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
@ -109,16 +109,16 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
qr_code_scanner_plus:
:path: ".symlinks/plugins/qr_code_scanner_plus/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
synced_shared_preferences:
:path: ".symlinks/plugins/synced_shared_preferences/ios"
url_launcher_ios:
@ -126,36 +126,36 @@ EXTERNAL SOURCES:
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
workmanager:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
in_app_review: 8efcf4a4d3ba72d5d776d29e5a268f1abf64d184
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
receive_sharing_intent: 753f808c6be5550247f6a20f2a14972466a5f33c
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
synced_shared_preferences: 90a2b479df93a2f6b68e08443f865ed1633dbe6a
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
PODFILE CHECKSUM: f03c7c11cf2b623592c89c68c628682778bb78b4
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View File

@ -22,7 +22,32 @@
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E573DDF82D3E273F00831A51 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E573DDF72D3E273F00831A51 /* WidgetKit.framework */; };
E573DDFA2D3E273F00831A51 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E573DDF92D3E273F00831A51 /* SwiftUI.framework */; };
E573DE072D3E274000831A51 /* Story Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E573DE392D3E282700831A51 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = E573DE382D3E282700831A51 /* Alamofire */; };
E573DE3E2D3E28CD00831A51 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = E573DE3D2D3E28CD00831A51 /* SwiftSoup */; };
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
E5CE971A2D3E541A00430A81 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97022D3E541A00430A81 /* ArrayExtension.swift */; };
E5CE971B2D3E541A00430A81 /* Date+TimeAgoString.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97032D3E541A00430A81 /* Date+TimeAgoString.swift */; };
E5CE971C2D3E541A00430A81 /* Int+OrZero.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97042D3E541A00430A81 /* Int+OrZero.swift */; };
E5CE971D2D3E541A00430A81 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97052D3E541A00430A81 /* StringExtension.swift */; };
E5CE971E2D3E541A00430A81 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97072D3E541A00430A81 /* Comment.swift */; };
E5CE971F2D3E541A00430A81 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97082D3E541A00430A81 /* Item.swift */; };
E5CE97202D3E541A00430A81 /* SearchFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97092D3E541A00430A81 /* SearchFilter.swift */; };
E5CE97212D3E541A00430A81 /* SearchParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE970A2D3E541A00430A81 /* SearchParams.swift */; };
E5CE97222D3E541A00430A81 /* Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE970B2D3E541A00430A81 /* Story.swift */; };
E5CE97232D3E541A00430A81 /* StoryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE970C2D3E541A00430A81 /* StoryType.swift */; };
E5CE97242D3E541A00430A81 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE970D2D3E541A00430A81 /* User.swift */; };
E5CE97252D3E541A00430A81 /* SelectStoryTypeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97112D3E541A00430A81 /* SelectStoryTypeIntent.swift */; };
E5CE97262D3E541A00430A81 /* StoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97122D3E541A00430A81 /* StoryEntry.swift */; };
E5CE97272D3E541A00430A81 /* StoryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97132D3E541A00430A81 /* StoryRepository.swift */; };
E5CE97282D3E541A00430A81 /* StorySource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97142D3E541A00430A81 /* StorySource.swift */; };
E5CE97292D3E541A00430A81 /* StoryTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97152D3E541A00430A81 /* StoryTimelineProvider.swift */; };
E5CE972A2D3E541A00430A81 /* StoryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97162D3E541A00430A81 /* StoryWidget.swift */; };
E5CE972B2D3E541A00430A81 /* StoryWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97172D3E541A00430A81 /* StoryWidgetBundle.swift */; };
E5CE972C2D3E541A00430A81 /* Timeline+Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE97182D3E541A00430A81 /* Timeline+Placeholder.swift */; };
E5CE972D2D3E541A00430A81 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5CE970F2D3E541A00430A81 /* Assets.xcassets */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -40,12 +65,19 @@
remoteGlobalIDString = E530B1A5283B54DA004E8EB6;
remoteInfo = "Action Extension";
};
E573DE052D3E274000831A51 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = E573DDF52D3E273F00831A51;
remoteInfo = StoryWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 12;
dstPath = "";
dstSubfolderSpec = 10;
files = (
@ -60,6 +92,7 @@
dstSubfolderSpec = 13;
files = (
E51D52B7283B464E00FC8DD8 /* Share Extension.appex in Embed App Extensions */,
E573DE072D3E274000831A51 /* Story Widget Extension.appex in Embed App Extensions */,
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
@ -96,9 +129,33 @@
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Story Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
E573DDF72D3E273F00831A51 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
E573DDF92D3E273F00831A51 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
E5CE97022D3E541A00430A81 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = "<group>"; };
E5CE97032D3E541A00430A81 /* Date+TimeAgoString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgoString.swift"; sourceTree = "<group>"; };
E5CE97042D3E541A00430A81 /* Int+OrZero.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+OrZero.swift"; sourceTree = "<group>"; };
E5CE97052D3E541A00430A81 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
E5CE97072D3E541A00430A81 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = "<group>"; };
E5CE97082D3E541A00430A81 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
E5CE97092D3E541A00430A81 /* SearchFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilter.swift; sourceTree = "<group>"; };
E5CE970A2D3E541A00430A81 /* SearchParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchParams.swift; sourceTree = "<group>"; };
E5CE970B2D3E541A00430A81 /* Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Story.swift; sourceTree = "<group>"; };
E5CE970C2D3E541A00430A81 /* StoryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryType.swift; sourceTree = "<group>"; };
E5CE970D2D3E541A00430A81 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
E5CE970F2D3E541A00430A81 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
E5CE97102D3E541A00430A81 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E5CE97112D3E541A00430A81 /* SelectStoryTypeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectStoryTypeIntent.swift; sourceTree = "<group>"; };
E5CE97122D3E541A00430A81 /* StoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryEntry.swift; sourceTree = "<group>"; };
E5CE97132D3E541A00430A81 /* StoryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryRepository.swift; sourceTree = "<group>"; };
E5CE97142D3E541A00430A81 /* StorySource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorySource.swift; sourceTree = "<group>"; };
E5CE97152D3E541A00430A81 /* StoryTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTimelineProvider.swift; sourceTree = "<group>"; };
E5CE97162D3E541A00430A81 /* StoryWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryWidget.swift; sourceTree = "<group>"; };
E5CE97172D3E541A00430A81 /* StoryWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryWidgetBundle.swift; sourceTree = "<group>"; };
E5CE97182D3E541A00430A81 /* Timeline+Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timeline+Placeholder.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -126,6 +183,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E573DDF32D3E273F00831A51 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E573DE392D3E282700831A51 /* Alamofire in Frameworks */,
E573DDFA2D3E273F00831A51 /* SwiftUI.framework in Frameworks */,
E573DDF82D3E273F00831A51 /* WidgetKit.framework in Frameworks */,
E573DE3E2D3E28CD00831A51 /* SwiftSoup in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -147,6 +215,7 @@
97C146F01CF9000F007C117D /* Runner */,
E51D52AE283B464E00FC8DD8 /* Share Extension */,
E530B1A9283B54DA004E8EB6 /* Action Extension */,
E5CE97192D3E541A00430A81 /* StoryWidget */,
97C146EF1CF9000F007C117D /* Products */,
D79CD63C88FF49EF451AFDDF /* Pods */,
B3F4F49CF582C662A01499C0 /* Frameworks */,
@ -159,6 +228,7 @@
97C146EE1CF9000F007C117D /* Runner.app */,
E51D52AD283B464E00FC8DD8 /* Share Extension.appex */,
E530B1A6283B54DA004E8EB6 /* Action Extension.appex */,
E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -185,6 +255,8 @@
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
E573DDF72D3E273F00831A51 /* WidgetKit.framework */,
E573DDF92D3E273F00831A51 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -222,6 +294,50 @@
path = "Action Extension";
sourceTree = "<group>";
};
E5CE97062D3E541A00430A81 /* Extensions */ = {
isa = PBXGroup;
children = (
E5CE97022D3E541A00430A81 /* ArrayExtension.swift */,
E5CE97032D3E541A00430A81 /* Date+TimeAgoString.swift */,
E5CE97042D3E541A00430A81 /* Int+OrZero.swift */,
E5CE97052D3E541A00430A81 /* StringExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
E5CE970E2D3E541A00430A81 /* Models */ = {
isa = PBXGroup;
children = (
E5CE97072D3E541A00430A81 /* Comment.swift */,
E5CE97082D3E541A00430A81 /* Item.swift */,
E5CE97092D3E541A00430A81 /* SearchFilter.swift */,
E5CE970A2D3E541A00430A81 /* SearchParams.swift */,
E5CE970B2D3E541A00430A81 /* Story.swift */,
E5CE970C2D3E541A00430A81 /* StoryType.swift */,
E5CE970D2D3E541A00430A81 /* User.swift */,
);
path = Models;
sourceTree = "<group>";
};
E5CE97192D3E541A00430A81 /* StoryWidget */ = {
isa = PBXGroup;
children = (
E5CE97062D3E541A00430A81 /* Extensions */,
E5CE970E2D3E541A00430A81 /* Models */,
E5CE970F2D3E541A00430A81 /* Assets.xcassets */,
E5CE97102D3E541A00430A81 /* Info.plist */,
E5CE97112D3E541A00430A81 /* SelectStoryTypeIntent.swift */,
E5CE97122D3E541A00430A81 /* StoryEntry.swift */,
E5CE97132D3E541A00430A81 /* StoryRepository.swift */,
E5CE97142D3E541A00430A81 /* StorySource.swift */,
E5CE97152D3E541A00430A81 /* StoryTimelineProvider.swift */,
E5CE97162D3E541A00430A81 /* StoryWidget.swift */,
E5CE97172D3E541A00430A81 /* StoryWidgetBundle.swift */,
E5CE97182D3E541A00430A81 /* Timeline+Placeholder.swift */,
);
path = StoryWidget;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -244,6 +360,7 @@
dependencies = (
E51D52B6283B464E00FC8DD8 /* PBXTargetDependency */,
E530B1B3283B54DA004E8EB6 /* PBXTargetDependency */,
E573DE062D3E274000831A51 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@ -284,13 +401,34 @@
productReference = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
E573DDF52D3E273F00831A51 /* Story Widget Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = E573DE0C2D3E274000831A51 /* Build configuration list for PBXNativeTarget "Story Widget Extension" */;
buildPhases = (
E573DDF22D3E273F00831A51 /* Sources */,
E573DDF32D3E273F00831A51 /* Frameworks */,
E573DDF42D3E273F00831A51 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "Story Widget Extension";
packageProductDependencies = (
E573DE382D3E282700831A51 /* Alamofire */,
E573DE3D2D3E28CD00831A51 /* SwiftSoup */,
);
productName = StoryWidgetExtension;
productReference = E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1330;
LastSwiftUpdateCheck = 1610;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@ -304,6 +442,9 @@
E530B1A5283B54DA004E8EB6 = {
CreatedOnToolsVersion = 13.3;
};
E573DDF52D3E273F00831A51 = {
CreatedOnToolsVersion = 16.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@ -315,6 +456,10 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
E573DE372D3E282700831A51 /* XCRemoteSwiftPackageReference "Alamofire" */,
E573DE3C2D3E28CD00831A51 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -322,6 +467,7 @@
97C146ED1CF9000F007C117D /* Runner */,
E51D52AC283B464E00FC8DD8 /* Share Extension */,
E530B1A5283B54DA004E8EB6 /* Action Extension */,
E573DDF52D3E273F00831A51 /* Story Widget Extension */,
);
};
/* End PBXProject section */
@ -355,6 +501,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E573DDF42D3E273F00831A51 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E5CE972D2D3E541A00430A81 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@ -372,7 +526,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@ -456,6 +610,32 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E573DDF22D3E273F00831A51 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E5CE971A2D3E541A00430A81 /* ArrayExtension.swift in Sources */,
E5CE971B2D3E541A00430A81 /* Date+TimeAgoString.swift in Sources */,
E5CE971C2D3E541A00430A81 /* Int+OrZero.swift in Sources */,
E5CE971D2D3E541A00430A81 /* StringExtension.swift in Sources */,
E5CE971E2D3E541A00430A81 /* Comment.swift in Sources */,
E5CE971F2D3E541A00430A81 /* Item.swift in Sources */,
E5CE97202D3E541A00430A81 /* SearchFilter.swift in Sources */,
E5CE97212D3E541A00430A81 /* SearchParams.swift in Sources */,
E5CE97222D3E541A00430A81 /* Story.swift in Sources */,
E5CE97232D3E541A00430A81 /* StoryType.swift in Sources */,
E5CE97242D3E541A00430A81 /* User.swift in Sources */,
E5CE97252D3E541A00430A81 /* SelectStoryTypeIntent.swift in Sources */,
E5CE97262D3E541A00430A81 /* StoryEntry.swift in Sources */,
E5CE97272D3E541A00430A81 /* StoryRepository.swift in Sources */,
E5CE97282D3E541A00430A81 /* StorySource.swift in Sources */,
E5CE97292D3E541A00430A81 /* StoryTimelineProvider.swift in Sources */,
E5CE972A2D3E541A00430A81 /* StoryWidget.swift in Sources */,
E5CE972B2D3E541A00430A81 /* StoryWidgetBundle.swift in Sources */,
E5CE972C2D3E541A00430A81 /* Timeline+Placeholder.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -469,6 +649,11 @@
target = E530B1A5283B54DA004E8EB6 /* Action Extension */;
targetProxy = E530B1B2283B54DA004E8EB6 /* PBXContainerItemProxy */;
};
E573DE062D3E274000831A51 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = E573DDF52D3E273F00831A51 /* Story Widget Extension */;
targetProxy = E573DE052D3E274000831A51 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -563,7 +748,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
@ -575,7 +759,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -706,7 +890,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
@ -718,7 +901,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -739,7 +922,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
@ -753,7 +935,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -789,7 +971,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -831,7 +1013,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -869,7 +1051,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -908,7 +1090,7 @@
INFOPLIST_FILE = "Action Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -952,7 +1134,7 @@
INFOPLIST_FILE = "Action Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -992,7 +1174,7 @@
INFOPLIST_FILE = "Action Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1010,6 +1192,135 @@
};
name = Profile;
};
E573DE082D3E274000831A51 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StoryWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = StoryWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Widget-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
E573DE092D3E274000831A51 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StoryWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = StoryWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Widget-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki.Widget-Extension";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
E573DE0A2D3E274000831A51 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StoryWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = StoryWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Widget-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -1053,7 +1364,49 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E573DE0C2D3E274000831A51 /* Build configuration list for PBXNativeTarget "Story Widget Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E573DE082D3E274000831A51 /* Debug */,
E573DE092D3E274000831A51 /* Release */,
E573DE0A2D3E274000831A51 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
E573DE372D3E282700831A51 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.2;
};
};
E573DE3C2D3E28CD00831A51 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.7.6;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
E573DE382D3E282700831A51 /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = E573DE372D3E282700831A51 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
E573DE3D2D3E28CD00831A51 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;
package = E573DE3C2D3E28CD00831A51 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E530B1A5283B54DA004E8EB6"
BuildableName = "Action Extension.appex"
BlueprintName = "Action Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@ -45,11 +46,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E51D52AC283B464E00FC8DD8"
BuildableName = "Share Extension.appex"
BlueprintName = "Share Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E573DDF52D3E273F00831A51"
BuildableName = "Story Widget Extension.appex"
BlueprintName = "Story Widget Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.springboard">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E573DDF52D3E273F00831A51"
BuildableName = "Story Widget Extension.appex"
BlueprintName = "Story Widget Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,24 @@
{
"originHash" : "1002c245c0fdae6ca9c33705b8fc0eaeec1eff55818735c136a20ed23937d94f",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "0837db354faf9c9deb710dc597046edaadf5360f",
"version" : "2.7.6"
}
}
],
"version" : 3
}

View File

@ -8,6 +8,8 @@
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>

View File

@ -3,187 +3,276 @@ import Social
import MobileCoreServices
import Photos
class ShareViewController: SLComposeServiceViewController {
let hostAppBundleIdentifier = "com.jiaqi.hacki"
let sharedKey = "ShareKey"
var sharedMedia: [SharedMediaFile] = []
var sharedText: [String] = []
let imageContentType = UTType.image
let videoContentType = UTType.movie
let textContentType = UTType.text
let urlContentType = UTType.url
let fileURLType = UTType.fileURL
public class SharedMediaFile: Codable {
var path: String
var mimeType: String?
var thumbnail: String? // video thumbnail
var duration: Double? // video duration in milliseconds
var message: String? // post message
var type: SharedMediaType
override func isContentValid() -> Bool {
public init(
path: String,
mimeType: String? = nil,
thumbnail: String? = nil,
duration: Double? = nil,
message: String?=nil,
type: SharedMediaType) {
self.path = path
self.mimeType = mimeType
self.thumbnail = thumbnail
self.duration = duration
self.message = message
self.type = type
}
}
public enum SharedMediaType: String, Codable, CaseIterable {
case image
case video
case text
case file
case url
public var toUTTypeIdentifier: String {
if #available(iOS 14.0, *) {
switch self {
case .image:
return UTType.image.identifier
case .video:
return UTType.movie.identifier
case .text:
return UTType.text.identifier
case .file:
return UTType.fileURL.identifier
case .url:
return UTType.url.identifier
}
}
switch self {
case .image:
return "public.image"
case .video:
return "public.movie"
case .text:
return "public.text"
case .file:
return "public.file-url"
case .url:
return "public.url"
}
}
}
let kSchemePrefix = "ShareMedia"
let kUserDefaultsKey = "ShareKey"
let kUserDefaultsMessageKey = "ShareMessageKey"
let kAppGroupIdKey = "AppGroupId"
class ShareViewController: SLComposeServiceViewController {
var hostAppBundleIdentifier = "com.jiaqi.hacki"
var appGroupId = "group.com.jiaqi.hacki"
var sharedMedia: [SharedMediaFile] = []
/// Override this method to return false if you don't want to redirect to host app automatically
/// Default is true
open func shouldAutoRedirect() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad();
open override func isContentValid() -> Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
open override func viewDidLoad() {
super.viewDidLoad()
// load group and app id from build info
loadIds()
}
// Redirect to host app when user click on Post
open override func didSelectPost() {
saveAndRedirect(message: contentText)
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
if let contents = content.attachments {
for (index, attachment) in (contents).enumerated() {
if attachment.hasItemConformingToTypeIdentifier(imageContentType.identifier) {
handleImages(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(textContentType.identifier) {
handleText(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(fileURLType.identifier) {
handleFiles(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(urlContentType.identifier) {
handleUrl(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(videoContentType.identifier) {
handleVideos(content: content, attachment: attachment, index: index)
for type in SharedMediaType.allCases {
if attachment.hasItemConformingToTypeIdentifier(type.toUTTypeIdentifier) {
attachment.loadItem(forTypeIdentifier: type.toUTTypeIdentifier) { [weak self] data, error in
guard let this = self, error == nil else {
self?.dismissWithError()
return
}
switch type {
case .text:
if let text = data as? String {
this.handleMedia(forLiteral: text,
type: type,
index: index,
content: content)
}
case .url:
if let url = data as? URL {
this.handleMedia(forLiteral: url.absoluteString,
type: type,
index: index,
content: content)
}
default:
if let url = data as? URL {
this.handleMedia(forFile: url,
type: type,
index: index,
content: content)
}
else if let image = data as? UIImage {
this.handleMedia(forUIImage: image,
type: type,
index: index,
content: content)
}
}
}
break
}
}
}
}
}
}
override func didSelectPost() {
print("didSelectPost");
}
override func configurationItems() -> [Any]! {
open override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: textContentType.identifier, options: nil) { [weak self] data, error in
private func loadIds() {
// loading Share extension App Id
let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!
// extract host app bundle id from ShareExtension id
// by default it's <hostAppBundleIdentifier>.<ShareExtension>
// for example: "com.kasem.sharing.Share-Extension" -> com.kasem.sharing
let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".")
hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[..<lastIndexOfPoint!])
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
// loading custom AppGroupId from Build Settings or use group.<hostAppBundleIdentifier>
let customAppGroupId = Bundle.main.object(forInfoDictionaryKey: kAppGroupIdKey) as? String
appGroupId = customAppGroupId ?? defaultAppGroupId
}
private func handleMedia(forLiteral item: String, type: SharedMediaType, index: Int, content: NSExtensionItem) {
sharedMedia.append(SharedMediaFile(
path: item,
mimeType: type == .text ? "text/plain": nil,
type: type
))
if index == (content.attachments?.count ?? 0) - 1 {
if shouldAutoRedirect() {
saveAndRedirect()
}
}
}
if error == nil, let item = data as? String, let this = self {
this.sharedText.append(item)
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .text)
}
} else {
self?.dismissWithError()
private func handleMedia(forUIImage image: UIImage, type: SharedMediaType, index: Int, content: NSExtensionItem){
let tempPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!.appendingPathComponent("TempImage.png")
if self.writeTempFile(image, to: tempPath) {
let newPathDecoded = tempPath.absoluteString.removingPercentEncoding!
sharedMedia.append(SharedMediaFile(
path: newPathDecoded,
mimeType: type == .image ? "image/png": nil,
type: type
))
}
if index == (content.attachments?.count ?? 0) - 1 {
if shouldAutoRedirect() {
saveAndRedirect()
}
}
}
private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: urlContentType.identifier, options: nil) { [weak self] data, error in
if error == nil, let item = data as? URL, let this = self {
this.sharedText.append(item.absoluteString)
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .text)
private func handleMedia(forFile url: URL, type: SharedMediaType, index: Int, content: NSExtensionItem) {
let fileName = getFileName(from: url, type: type)
let newPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!.appendingPathComponent(fileName)
if copyFile(at: url, to: newPath) {
// The path should be decoded because Flutter is not expecting url encoded file names
let newPathDecoded = newPath.absoluteString.removingPercentEncoding!;
if type == .video {
// Get video thumbnail and duration
if let videoInfo = getVideoInfo(from: url) {
let thumbnailPathDecoded = videoInfo.thumbnail?.removingPercentEncoding;
sharedMedia.append(SharedMediaFile(
path: newPathDecoded,
mimeType: url.mimeType(),
thumbnail: thumbnailPathDecoded,
duration: videoInfo.duration,
type: type
))
}
} else {
self?.dismissWithError()
sharedMedia.append(SharedMediaFile(
path: newPathDecoded,
mimeType: url.mimeType(),
type: type
))
}
}
if index == (content.attachments?.count ?? 0) - 1 {
if shouldAutoRedirect() {
saveAndRedirect()
}
}
}
private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: imageContentType.identifier, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from: url, type: .image)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image))
}
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .media)
}
} else {
self?.dismissWithError()
}
}
// Save shared media and redirect to host app
private func saveAndRedirect(message: String? = nil) {
let userDefaults = UserDefaults(suiteName: appGroupId)
userDefaults?.set(toData(data: sharedMedia), forKey: kUserDefaultsKey)
userDefaults?.set(message, forKey: kUserDefaultsMessageKey)
userDefaults?.synchronize()
redirectToHostApp()
}
private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: videoContentType.identifier, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from: url, type: .video)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
return
}
this.sharedMedia.append(sharedFile)
private func redirectToHostApp() {
// ids may not loaded yet so we need loadIds here too
loadIds()
let url = URL(string: "\(kSchemePrefix)-\(hostAppBundleIdentifier):share")
var responder = self as UIResponder?
if #available(iOS 18.0, *) {
while responder != nil {
if let application = responder as? UIApplication {
application.open(url!, options: [:], completionHandler: nil)
}
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .media)
responder = responder?.next
}
} else {
let selectorOpenURL = sel_registerName("openURL:")
while (responder != nil) {
if (responder?.responds(to: selectorOpenURL))! {
_ = responder?.perform(selectorOpenURL, with: url)
}
} else {
self?.dismissWithError()
responder = responder!.next
}
}
}
private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: fileURLType.identifier, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from :url, type: .file)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if (copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
}
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .file)
}
} else {
self?.dismissWithError()
}
}
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func dismissWithError() {
@ -199,57 +288,38 @@ class ShareViewController: SLComposeServiceViewController {
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func redirectToHostApp(type: RedirectType) {
let url = URL(string: "ShareMedia-\(hostAppBundleIdentifier)://dataUrl=\(sharedKey)#\(type)")
var responder = self as UIResponder?
let selectorOpenURL = sel_registerName("openURL:")
while (responder != nil) {
if (responder?.responds(to: selectorOpenURL))! {
let _ = responder?.perform(selectorOpenURL, with: url)
}
responder = responder!.next
}
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
enum RedirectType {
case media
case text
case file
}
func getExtension(from url: URL, type: SharedMediaType) -> String {
let parts = url.lastPathComponent.components(separatedBy: ".")
var ex: String? = nil
if (parts.count > 1) {
ex = parts.last
}
if (ex == nil) {
private func getFileName(from url: URL, type: SharedMediaType) -> String {
var name = url.lastPathComponent
if name.isEmpty {
switch type {
case .image:
ex = "PNG"
name = UUID().uuidString + ".png"
case .video:
ex = "MP4"
case .file:
ex = "TXT"
name = UUID().uuidString + ".mp4"
case .text:
name = UUID().uuidString + ".txt"
default:
name = UUID().uuidString
}
}
return ex ?? "Unknown"
}
func getFileName(from url: URL, type: SharedMediaType) -> String {
var name = url.lastPathComponent
if (name.isEmpty) {
name = UUID().uuidString + "." + getExtension(from: url, type: type)
}
return name
}
private func writeTempFile(_ image: UIImage, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
let pngData = image.pngData();
try pngData?.write(to: dstURL);
return true;
} catch (let error){
print("Cannot write to temp file: \(error)");
return false;
}
}
func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
private func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
@ -262,13 +332,13 @@ class ShareViewController: SLComposeServiceViewController {
return true
}
private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? {
let asset = AVAsset(url: forVideo)
private func getVideoInfo(from url: URL) -> (thumbnail: String?, duration: Double)? {
let asset = AVAsset(url: url)
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
let thumbnailPath = getThumbnailPath(for: forVideo)
let thumbnailPath = getThumbnailPath(for: url)
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video)
return (thumbnail: thumbnailPath.absoluteString, duration: duration)
}
var saved = false
@ -277,59 +347,44 @@ class ShareViewController: SLComposeServiceViewController {
// let scale = UIScreen.main.scale
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
do {
let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil)
try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath)
let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: 1), actualTime: nil)
try UIImage(cgImage: img).pngData()?.write(to: thumbnailPath)
saved = true
} catch {
saved = false
}
return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil
return saved ? (thumbnail: thumbnailPath.absoluteString, duration: duration): nil
}
private func getThumbnailPath(for url: URL) -> URL {
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
let path = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!
.appendingPathComponent("\(fileName).jpg")
return path
}
class SharedMediaFile: Codable {
var path: String; // can be image, video or url path. It can also be text content
var thumbnail: String?; // video thumbnail
var duration: Double?; // video duration in milliseconds
var type: SharedMediaType;
init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) {
self.path = path
self.thumbnail = thumbnail
self.duration = duration
self.type = type
}
// Debug method to print out SharedMediaFile details in the console
func toString() {
print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(String(describing: self.thumbnail))\n\tduration: \(String(describing: self.duration))\n\ttype: \(self.type)")
}
}
enum SharedMediaType: Int, Codable {
case image
case video
case file
}
func toData(data: [SharedMediaFile]) -> Data {
private func toData(data: [SharedMediaFile]) -> Data {
let encodedData = try? JSONEncoder().encode(data)
return encodedData!
}
}
extension Array {
subscript (safe index: UInt) -> Element? {
return Int(index) < count ? self[Int(index)] : nil
extension URL {
public func mimeType() -> String {
if #available(iOS 14.0, *) {
if let mimeType = UTType(filenameExtension: self.pathExtension)?.preferredMIMEType {
return mimeType
}
} else {
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, self.pathExtension as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
}
return "application/octet-stream"
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,36 @@
import Foundation
extension Optional where Wrapped: Collection {
var isMoreThanOne: Bool {
guard let unwrapped = self else {
return false
}
if unwrapped.count > 1 {
return true
} else {
return false
}
}
var isNullOrEmpty: Bool {
guard let unwrapped = self else {
return true
}
return unwrapped.isEmpty
}
var isNotNullOrEmpty: Bool {
return !isNullOrEmpty
}
var countOrZero: Int {
guard let unwrapped = self else {
return 0
}
return unwrapped.count
}
}

View File

@ -0,0 +1,9 @@
import Foundation
extension Date {
var timeAgoString: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter.localizedString(for: self, relativeTo: Date())
}
}

View File

@ -0,0 +1,10 @@
import Foundation
extension Int? {
var orZero: Int {
guard let unwrapped = self else {
return 0
}
return unwrapped
}
}

View File

@ -0,0 +1,88 @@
import Foundation
import UIKit
import SwiftSoup
public extension String {
var isNotEmpty: Bool {
!isEmpty
}
var htmlStripped: String {
do {
let pRegex = try Regex("<p>")
let iRegex = try Regex(#"\<i\>(.*?)\<\/i\>"#)
let codeRegex = try Regex(#"\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>"#)
.dotMatchesNewlines(true)
let linkRegex = try Regex(#"\<a href=\"(.*?)\".*?\>.*?\<\/a\>"#)
let res = try Entities.unescape(self)
.replacing(pRegex, with: { match in
"\n"
})
.replacing(iRegex, with: { match in
if let m = match[1].substring {
let matchedStr = String(m)
return "**\(matchedStr)**"
}
return String()
})
.replacing(linkRegex, with: { match in
if let m = match[1].substring {
let matchedStr = String(m)
return matchedStr
}
return String()
})
.withExtraLineBreak
.replacing(codeRegex, with: { match in
if let m = match[1].substring {
let matchedStr = String(m)
return "```" + String(matchedStr.replacing("\n\n", with: "``` \n ``` \n").dropLast(1)) + "```"
}
return String()
})
return res
} catch {
return error.localizedDescription
}
}
func toJSON() -> Any? {
guard let data = self.data(using: .utf8, allowLossyConversion: false) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
}
private var withExtraLineBreak: String {
if isEmpty { return self }
let range = startIndex..<index(endIndex, offsetBy: -1)
var str = String(replacingOccurrences(of: "\n", with: "\n\n", range: range))
while str.last?.isWhitespace == true {
str = String(str.dropLast())
}
return str
}
}
public extension Optional where Wrapped == String {
var orEmpty: String {
guard let unwrapped = self else {
return ""
}
return unwrapped
}
var htmlStripped: String{
guard let unwrapped = self else {
return ""
}
return unwrapped.htmlStripped
}
var isNotNullOrEmpty: Bool {
guard let unwrapped = self else {
return false
}
return unwrapped.isNotEmpty
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,54 @@
public struct Comment: Item {
public let id: Int
public let parent: Int?
public let text: String?
public let type: String?
public let by: String?
public let time: Int
public let kids: [Int]?
public let level: Int?
public var metadata: String {
if let count = kids?.count, count != 0 {
return "\(count) cmt\(count > 1 ? "s":"") | \(timeAgo) by \(by.orEmpty)"
} else {
return "\(timeAgo) by \(by.orEmpty)"
}
}
/// Values below will always be nil for `Comment`.
public let title: String?
public let url: String?
public let descendants: Int?
public let score: Int?
init(id: Int, parent: Int?, text: String?, by: String?, time: Int, kids: [Int]? = [Int](), level: Int? = 0) {
self.id = id
self.parent = parent
self.text = text
self.by = by
self.time = time
self.kids = kids
self.level = level
self.type = "comment"
self.title = nil
self.url = nil
self.descendants = nil
self.score = nil
}
// Empty initializer
init() {
self.init(id: 0, parent: 0, text: "", by: "", time: 0)
}
public func copyWith(text: String? = nil, level: Int? = nil) -> Comment {
Comment(id: id,
parent: parent,
text: text ?? self.text,
by: by,
time: time,
kids: kids ?? [Int](),
level: level ?? self.level)
}
}

View File

@ -0,0 +1,59 @@
import Foundation
public protocol Item: Codable, Identifiable, Hashable {
var id: Int { get }
var parent: Int? { get }
var title: String? { get }
var text: String? { get }
var url: String? { get }
var type: String? { get }
var by: String? { get }
var score: Int? { get }
var descendants: Int? { get }
var time: Int { get }
var kids: [Int]? { get }
var metadata: String { get }
}
public extension Item {
var createdAtDate: Date {
let date = Date(timeIntervalSince1970: Double(time))
return date
}
var createdAt: String {
let date = Date(timeIntervalSince1970: Double(time))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d, yyyy"
return dateFormatter.string(from: date)
}
var timeAgo: String {
let date = Date(timeIntervalSince1970: Double(time))
return date.timeAgoString
}
var formattedTime: String {
Date(timeIntervalSince1970: Double(time)).formatted()
}
var itemUrl: String {
"https://news.ycombinator.com/item?id=\(self.id)"
}
var readableUrl: String? {
if let url = self.url {
let domain = URL(string: url)?.host
return domain
}
return nil
}
var isJob: Bool {
return type == "job"
}
var isJobWithUrl: Bool {
return type == "job" && text.isNullOrEmpty && url.isNotNullOrEmpty
}
}

View File

@ -0,0 +1,48 @@
import Foundation
public enum SearchFilter: Equatable, Hashable {
case story
case comment
case dateRange(Date, Date)
var query: String {
switch(self){
case .story:
return "story"
case .comment:
return "comment"
case .dateRange(let startDate, let endDate):
let startTimestamp = Int(startDate.timeIntervalSince1970.rounded())
let endTimestamp = Int(endDate.timeIntervalSince1970.rounded())
if startTimestamp != endTimestamp {
return "created_at_i>=\(startTimestamp),created_at_i<=\(endTimestamp)"
} else {
let updatedStartDate = Calendar.current.date(
byAdding: .hour,
value: -24,
to: startDate)
let updatedStartTimestamp = updatedStartDate?.timeIntervalSince1970
if let updatedStartTimestamp = updatedStartTimestamp?.rounded() {
return "created_at_i>=\(Int(updatedStartTimestamp)),created_at_i<=\(endTimestamp)"
}
return .init()
}
}
}
var isNumericFilter: Bool {
switch(self){
case .story, .comment:
return false
case .dateRange:
return true
}
}
var isTagFilter: Bool {
!isNumericFilter
}
}

View File

@ -0,0 +1,62 @@
import Foundation
public class SearchParams: Equatable {
public let page: Int
public let query: String
public let sorted: Bool
public let filters: Set<SearchFilter>
public var filteredQuery: String {
var buffer = String()
if sorted {
buffer.append("search_by_date?query=\(query)")
} else {
buffer.append("search?query=\(query)")
}
if !filters.isEmpty {
let numericFilters = filters.filter({ $0.isNumericFilter })
let tagFilters = filters.filter({ $0.isTagFilter })
if !numericFilters.isEmpty {
buffer.append("&numericFilters=")
for filter in filters.filter({ $0.isNumericFilter }) {
buffer.append(filter.query)
buffer.append(",")
}
buffer = String(buffer.dropLast(1))
}
if !tagFilters.isEmpty {
buffer.append("&tags=(")
for filter in filters.filter({ $0.isTagFilter }) {
buffer.append(filter.query)
buffer.append(",")
}
buffer = String(buffer.dropLast(1))
buffer.append(")")
}
}
buffer.append("&page=\(page)");
return buffer
}
public init(page: Int, query: String, sorted: Bool, filters: Set<SearchFilter>) {
self.page = page
self.query = query
self.sorted = sorted
self.filters = filters
}
public func copyWith(page: Int? = nil, query: String? = nil, sorted: Bool? = nil, filters: Set<SearchFilter>? = nil) -> SearchParams {
return SearchParams(page: page ?? self.page, query: query ?? self.query, sorted: sorted ?? self.sorted, filters: filters ?? self.filters)
}
public static func == (lhs: SearchParams, rhs: SearchParams) -> Bool {
return lhs.page == rhs.page && lhs.query == rhs.query && lhs.sorted == rhs.sorted && lhs.filters == rhs.filters
}
}

View File

@ -0,0 +1,98 @@
public extension Story {
var shortMetadata: String {
if isJob {
return "\(timeAgo)"
} else {
return "\(score.orZero) | \(descendants.orZero) | \(timeAgo)"
}
}
}
public struct Story: Item {
public let id: Int
public let parent: Int?
public let title: String?
public let text: String?
public let url: String?
public let type: String?
public let by: String?
public let score: Int?
public let descendants: Int?
public let time: Int
public let kids: [Int]?
public var metadata: String {
if isJob {
return "\(timeAgo) by \(by.orEmpty)"
} else {
return "\(score.orZero) pt\(score.orZero > 1 ? "s":"") | \(descendants.orZero) cmt\(descendants.orZero > 1 ? "s":"") | \(timeAgo) by \(by.orEmpty)"
}
}
public init(id: Int,
parent: Int? = nil,
title: String?,
text: String?,
url: String?,
type: String?,
by: String?,
score: Int?,
descendants: Int?,
time: Int,
kids: [Int]? = [Int]()) {
self.id = id
self.parent = parent
self.title = title
self.text = text
self.url = url
self.type = type
self.score = score
self.by = by
self.descendants = descendants
self.time = time
self.kids = kids
}
// Empty initializer
public init() {
self.init(
id: 0,
parent: 0,
title: "",
text: "",
url: "",
type: "story",
by: "",
score: 0,
descendants: 0,
time: 0
)
}
func copyWith(text: String?) -> Story {
.init(
id: id,
parent: parent,
title: title,
text: text,
url: url,
type: type,
by: by,
score: score,
descendants: descendants,
time: time,
kids: kids
)
}
public static let errorPlaceholder: Story = .init(
id: 0,
title: "Something went wrong...",
text: nil,
url: "retrying...",
type: "story",
by: nil,
score: nil,
descendants: nil,
time: 0
)
}

View File

@ -0,0 +1,56 @@
import AppIntents
import SwiftData
public enum StoryType: String, Equatable, CaseIterable, AppEnum, Codable {
case top = "top"
case best = "best"
case new = "new"
case ask = "ask"
case show = "show"
case jobs = "job"
public var icon: String {
switch self {
case .top:
return "flame"
case .best:
return "medal"
case .new:
return "rectangle.dashed"
case .ask:
return "questionmark.bubble"
case .show:
return "sparkles.tv"
case .jobs:
return "briefcase"
}
}
public var label: String {
switch self {
case .jobs:
return "jobs"
default:
return self.rawValue
}
}
public var isDownloadable: Bool {
switch self {
case .top, .ask, .best:
return true
default:
return false
}
}
public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Story Type"
public static var caseDisplayRepresentations: [StoryType : DisplayRepresentation] = [
.top: "Top",
.best: "Best",
.new: "New",
.ask: "Ask HN",
.show: "Show HN",
.jobs: "YC Jobs"
]
}

View File

@ -0,0 +1,53 @@
import Foundation
public struct User: Decodable, Equatable {
public let id: String?
public let about: String?
public let created: Int?
public let delay: Int?
public let karma: Int?
public let submitted: [Int]?
public init() {
self.id = .init()
self.about = .init()
self.created = .init()
self.delay = .init()
self.karma = .init()
self.submitted = .init()
}
/// If a user does not have any activity, the user endpoint will not return anything.
/// in that case, we create a user with only username.
public init(id: String) {
self.id = id
self.about = .init()
self.created = .init()
self.delay = .init()
self.karma = .init()
self.submitted = .init()
}
init(id: String?, about: String?, created: Int?, delay: Int?, karma: Int?, submitted: [Int]?) {
self.id = id
self.about = about
self.created = created
self.delay = delay
self.karma = karma
self.submitted = submitted
}
func copyWith(about: String? = nil) -> User {
return User(id: id, about: about ?? self.about, created: created, delay: delay, karma: karma, submitted: submitted)
}
}
public extension User {
var createdAt: String? {
guard let created = created else { return nil }
let date = Date(timeIntervalSince1970: Double(created))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d, yyyy"
return dateFormatter.string(from: date)
}
}

View File

@ -0,0 +1,17 @@
import AppIntents
struct SelectStoryTypeIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Story Type"
static var description = IntentDescription("Select the type of story you want to see.")
@Parameter(title: "Story Type", default: StorySource.top)
var source: StorySource
init(source: StorySource) {
self.source = source
}
init() {
self.source = .top
}
}

View File

@ -0,0 +1,14 @@
import WidgetKit
import Foundation
struct StoryEntry: TimelineEntry {
let date: Date
let story: Story
let source: StorySource
static let errorPlaceholder: StoryEntry = StoryEntry(
date: .now,
story: .errorPlaceholder,
source: .top
)
}

View File

@ -0,0 +1,130 @@
import Foundation
import Alamofire
public class StoryRepository {
public static let shared: StoryRepository = .init()
private let baseUrl: String = "https://hacker-news.firebaseio.com/v0/"
private init() {}
// MARK: - Story related.
public func fetchAllStories(from storyType: StoryType, onStoryFetched: @escaping (Story) -> Void) async -> Void {
let storyIds = await fetchStoryIds(from: storyType)
for id in storyIds {
let story = await self.fetchStory(id)
if let story = story {
onStoryFetched(story)
}
}
}
public func fetchStoryIds(from storyType: StoryType) async -> [Int] {
let response = await AF.request("\(self.baseUrl)\(storyType.rawValue)stories.json").serializingString().response
guard response.data != nil else { return [Int]() }
let storyIds = try? JSONDecoder().decode([Int].self, from: response.data!)
return storyIds ?? [Int]()
}
public func fetchStoryIds(from storyType: String) async -> [Int] {
let response = await AF.request("\(self.baseUrl)\(storyType)stories.json").serializingString().response
guard response.data != nil else { return [Int]() }
let storyIds = try? JSONDecoder().decode([Int].self, from: response.data!)
return storyIds ?? [Int]()
}
public func fetchStories(ids: [Int], onStoryFetched: @escaping (Story) -> Void) async -> Void {
for id in ids {
let story = await fetchStory(id)
if let story = story {
onStoryFetched(story)
}
}
}
public func fetchStory(_ id: Int) async -> Story?{
let response = await AF.request("\(self.baseUrl)item/\(id).json").serializingString().response
if let data = response.data,
var story = try? JSONDecoder().decode(Story.self, from: data) {
let formattedText = story.text.htmlStripped
story = story.copyWith(text: formattedText)
return story
} else {
return nil
}
}
// MARK: - Comment related.
public func fetchComments(ids: [Int], onCommentFetched: @escaping (Comment) -> Void) async -> Void {
for id in ids {
let comment = await fetchComment(id)
if let comment = comment {
onCommentFetched(comment)
}
}
}
public func fetchComment(_ id: Int) async -> Comment? {
let response = await AF.request("\(self.baseUrl)item/\(id).json").serializingString().response
if let data = response.data,
var comment = try? JSONDecoder().decode(Comment.self, from: data) {
let formattedText = comment.text.htmlStripped
comment = comment.copyWith(text: formattedText)
return comment
} else {
return nil
}
}
// MARK: - Item related.
public func fetchItems(ids: [Int], filtered: Bool = true, onItemFetched: @escaping (any Item) -> Void) async -> Void {
for id in ids {
let item = await fetchItem(id)
guard let item = item else { continue }
if let story = item as? Story {
onItemFetched(story)
} else if let cmt = item as? Comment {
onItemFetched(cmt)
}
}
}
public func fetchItem(_ id: Int) async -> (any Item)? {
let response = await AF.request("\(self.baseUrl)item/\(id).json").serializingString().response
if let data = response.data,
let result = try? response.result.get(),
let map = result.toJSON() as? [String: AnyObject],
let type = map["type"] as? String {
switch type {
case "story":
let story = try? JSONDecoder().decode(Story.self, from: data)
let formattedText = story?.text.htmlStripped
return story?.copyWith(text: formattedText)
case "comment":
let comment = try? JSONDecoder().decode(Comment.self, from: data)
let formattedText = comment?.text.htmlStripped
return comment?.copyWith(text: formattedText)
default:
return nil
}
} else {
return nil
}
}
// MARK: - User related.
public func fetchUser(_ id: String) async -> User? {
let response = await AF.request("\(self.baseUrl)/user/\(id).json").serializingString().response
if let data = response.data,
let user = try? JSONDecoder().decode(User.self, from: data) {
let formattedText = user.about.orEmpty.htmlStripped
return user.copyWith(about: formattedText)
} else {
return nil
}
}
}

View File

@ -0,0 +1,21 @@
import AppIntents
enum StorySource: String, AppEnum {
case top
case best
case new
case ask
case show
case job
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Story Source"
static var caseDisplayRepresentations: [StorySource : DisplayRepresentation] = [
.top: "Top",
.best: "Best",
.new: "New",
.ask: "Ask",
.show: "Show",
.job: "Jobs"
]
}

View File

@ -0,0 +1,42 @@
import WidgetKit
struct StoryTimelineProvider: AppIntentTimelineProvider {
func snapshot(for configuration: SelectStoryTypeIntent, in context: Context) async -> StoryEntry {
let ids = await StoryRepository.shared.fetchStoryIds(from: configuration.source.rawValue)
guard let first = ids.first else { return .errorPlaceholder }
let story = await StoryRepository.shared.fetchStory(first)
guard let story = story else { return .errorPlaceholder }
let entry = StoryEntry(date: Date(), story: story, source: configuration.source)
return entry
}
func placeholder(in context: Context) -> StoryEntry {
let story = Story(
id: 0,
title: "This is a placeholder story",
text: "text",
url: "",
type: "story",
by: "Hacki",
score: 100,
descendants: 24,
time: Int(Date().timeIntervalSince1970)
)
return StoryEntry(date: Date(), story: story, source: .top)
}
func timeline(for configuration: SelectStoryTypeIntent, in context: Context) async -> Timeline<StoryEntry> {
let ids = await StoryRepository.shared.fetchStoryIds(from: configuration.source.rawValue)
guard let first = ids.first else {
return Timeline(entries: [.errorPlaceholder], policy: .atEnd)
}
let story = await StoryRepository.shared.fetchStory(first)
guard let story = story else {
return Timeline(entries: [.errorPlaceholder], policy: .atEnd)
}
let entry = StoryEntry(date: Date(), story: story, source: configuration.source)
let timeline = Timeline(entries: [entry], policy: .atEnd)
return timeline
}
}

View File

@ -0,0 +1,84 @@
import WidgetKit
import SwiftUI
import AppIntents
struct StoryWidgetView : View {
@Environment(\.widgetFamily) var family
@Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
var story: Story
var source: StorySource
var body: some View {
switch family {
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 0) {
Text(story.shortMetadata)
.font(.caption)
Text(story.title.orEmpty)
.font(.caption).fontWeight(.bold)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
Spacer(minLength: 0)
}
.containerBackground(for: .widget) {
Color(UIColor.secondarySystemBackground)
}
.widgetURL(URL(string: "/item/\(story.id)"))
default:
HStack {
VStack {
Text(story.title.orEmpty)
.font(family == .systemSmall ? .system(size: 14) : .body)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
if let text = story.text, text.isNotEmpty {
HStack {
Text(text.replacingOccurrences(of: "\n", with: " "))
.font(.footnote)
.lineLimit(3)
.foregroundColor(.gray)
Spacer()
}
}
Spacer()
HStack {
if let url = story.readableUrl {
Text(url)
.font(family == .systemSmall ? .system(size: 8) : .footnote)
.foregroundColor(.orange)
}
Spacer()
}
Divider().frame(maxWidth: .infinity)
HStack(alignment: .center) {
Text("\(source.rawValue.uppercased()) | \(story.metadata)")
.font(family == .systemSmall ? showsWidgetContainerBackground ? .system(size: 10) : .system(size: 8) : .caption)
.padding(.top, showsWidgetContainerBackground ? 2 : .zero)
Spacer()
}
}
}
.padding(.all, showsWidgetContainerBackground ? .zero : nil)
.containerBackground(for: .widget) {
Color(UIColor.secondarySystemBackground)
}
.widgetURL(URL(string: "/item/\(story.id)"))
}
}
}
struct StoryWidget: Widget {
let kind: String = "StoryWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: SelectStoryTypeIntent.self,
provider: StoryTimelineProvider()) { entry in
StoryWidgetView(story: entry.story, source: entry.source)
}
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
.configurationDisplayName("Story on Hacker News")
.description("Watch out. It's hot.")
}
}

View File

@ -0,0 +1,9 @@
import WidgetKit
import SwiftUI
@main
struct StoryWidgetBundle: WidgetBundle {
var body: some Widget {
StoryWidget()
}
}

View File

@ -0,0 +1,8 @@
import WidgetKit
extension Timeline where EntryType == StoryEntry {
static let errorPlaceholder: Timeline<StoryEntry> = .init(
entries: [.errorPlaceholder],
policy: .atEnd
)
}

View File

@ -0,0 +1,18 @@
import AppIntents
import HackerNewsKit
struct SelectStoryTypeIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Story Type"
static var description = IntentDescription("Select the type of story you want to see.")
@Parameter(title: "Story Type", default: StorySource.top)
var source: StorySource
init(source: StorySource) {
self.source = source
}
init() {
self.source = .top
}
}

View File

@ -0,0 +1,21 @@
import AppIntents
enum StorySource: String, AppEnum {
case top
case best
case new
case ask
case show
case job
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Story Source"
static var caseDisplayRepresentations: [StorySource : DisplayRepresentation] = [
.top: "Top",
.best: "Best",
.new: "New",
.ask: "Ask",
.show: "Show",
.job: "Jobs"
]
}

View File

@ -1,5 +1,5 @@
app_identifier("com.jiaqi.hacki") # The bundle identifier of your app
apple_id("georgefung78@Live.com") # Your Apple Developer Portal username
apple_id("georgefung78@live.com") # Your Apple Developer Portal username
itc_team_id("120097292") # App Store Connect Team ID
team_id("QMWX3X2NF7") # Developer Portal Team ID

View File

@ -34,7 +34,7 @@ platform :ios do
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing(
type: "appstore",
app_identifier: [APP_IDENTIFIER, "#{APP_IDENTIFIER}.Share-Extension", "#{APP_IDENTIFIER}.Action-Extension"],
app_identifier: [APP_IDENTIFIER, "#{APP_IDENTIFIER}.Share-Extension", "#{APP_IDENTIFIER}.Action-Extension", "#{APP_IDENTIFIER}.Widget-Extension"],
readonly: true
)

View File

@ -14,7 +14,6 @@ import 'package:responsive_builder/responsive_builder.dart';
import 'package:rxdart/rxdart.dart';
part 'stories_event.dart';
part 'stories_state.dart';
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
@ -55,6 +54,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
on<StoriesEnterOfflineMode>(onEnterOfflineMode);
on<StoriesExitOfflineMode>(onExitOfflineMode);
on<ClearAllReadStories>(onClearAllReadStories);
on<UpdateMaxOfflineStoriesCount>(onUpdateMaxOfflineStoriesCount);
on<ClearMaxOfflineStoriesCount>(onClearMaxOfflineStoriesCount);
_preferenceSubscription = _preferenceCubit.stream
.distinct((PreferenceState lhs, PreferenceState rhs) {
@ -350,12 +351,16 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
await _offlineRepository.deleteAllComments();
await _offlineRepository.deleteAllWebPages();
final Set<int> prioritizedIds = <int>{};
List<int> prioritizedIds = <int>[];
/// Prioritizing all types of stories except StoryType.latest since
/// new stories tend to have less or no comment at all.
final List<StoryType> prioritizedTypes = <StoryType>[...StoryType.values]
..remove(StoryType.latest);
final List<StoryType> prioritizedTypes = <StoryType>[
StoryType.top,
StoryType.best,
StoryType.ask,
StoryType.show,
];
for (final StoryType type in prioritizedTypes) {
final List<int> ids =
@ -364,6 +369,14 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
prioritizedIds.addAll(ids);
}
prioritizedIds = prioritizedIds.toSet().toList().sublist(
0,
min(
state.maxOfflineStoriesCount?.count ?? prioritizedIds.length,
prioritizedIds.length,
),
);
emit(
state.copyWith(
storiesDownloaded: 0,
@ -539,6 +552,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
add(StoriesInitialize());
}
Future<void> onUpdateMaxOfflineStoriesCount(
UpdateMaxOfflineStoriesCount event,
Emitter<StoriesState> emit,
) async {
emit(state.copyWith(maxOfflineStoriesCount: event.count));
}
Future<void> onClearMaxOfflineStoriesCount(
ClearMaxOfflineStoriesCount event,
Emitter<StoriesState> emit,
) async {
emit(state.copyWithMaxOfflineStoriesCountReset());
}
Future<void> onStoryRead(
StoryRead event,
Emitter<StoriesState> emit,

View File

@ -104,6 +104,22 @@ class StoriesEnterOfflineMode extends StoriesEvent {
List<Object?> get props => <Object?>[];
}
class UpdateMaxOfflineStoriesCount extends StoriesEvent {
UpdateMaxOfflineStoriesCount({required this.count});
final MaxOfflineStoriesCount count;
@override
List<Object?> get props => <Object?>[count];
}
class ClearMaxOfflineStoriesCount extends StoriesEvent {
ClearMaxOfflineStoriesCount();
@override
List<Object?> get props => <Object?>[];
}
class StoryLoaded extends StoriesEvent {
StoryLoaded({required this.story, required this.type});

View File

@ -19,6 +19,7 @@ class StoriesState extends Equatable {
required this.downloadStatus,
required this.storiesDownloaded,
required this.storiesToBeDownloaded,
required this.maxOfflineStoriesCount,
required this.dataSource,
});
@ -56,6 +57,7 @@ class StoriesState extends Equatable {
readStoriesIds = const <int>{},
storiesDownloaded = 0,
storiesToBeDownloaded = 0,
maxOfflineStoriesCount = null,
dataSource = null;
final Map<StoryType, List<Story>> storiesByType;
@ -67,6 +69,7 @@ class StoriesState extends Equatable {
final bool isOfflineReading;
final int storiesDownloaded;
final int storiesToBeDownloaded;
final MaxOfflineStoriesCount? maxOfflineStoriesCount;
final HackerNewsDataSource? dataSource;
StoriesState copyWith({
@ -79,6 +82,7 @@ class StoriesState extends Equatable {
bool? isOfflineReading,
int? storiesDownloaded,
int? storiesToBeDownloaded,
MaxOfflineStoriesCount? maxOfflineStoriesCount,
HackerNewsDataSource? dataSource,
}) {
return StoriesState(
@ -93,6 +97,24 @@ class StoriesState extends Equatable {
storiesToBeDownloaded:
storiesToBeDownloaded ?? this.storiesToBeDownloaded,
dataSource: dataSource ?? this.dataSource,
maxOfflineStoriesCount:
maxOfflineStoriesCount ?? this.maxOfflineStoriesCount,
);
}
StoriesState copyWithMaxOfflineStoriesCountReset() {
return StoriesState(
storiesByType: storiesByType,
storyIdsByType: storyIdsByType,
statusByType: statusByType,
currentPageByType: currentPageByType,
readStoriesIds: readStoriesIds,
isOfflineReading: isOfflineReading,
downloadStatus: downloadStatus,
storiesDownloaded: storiesDownloaded,
storiesToBeDownloaded: storiesToBeDownloaded,
dataSource: dataSource,
maxOfflineStoriesCount: null,
);
}
@ -182,5 +204,6 @@ class StoriesState extends Equatable {
storiesDownloaded,
storiesToBeDownloaded,
dataSource,
maxOfflineStoriesCount,
];
}

View File

@ -43,7 +43,7 @@ abstract class Constants {
'٩(˘◡˘)۶',
'(─‿‿─)',
'(¬‿¬)',
].pickRandomly()!;
].randomlyPicked!;
static final String sadFace = <String>[
'ಥ_ಥ',
@ -55,13 +55,13 @@ abstract class Constants {
'(ㆆ_ㆆ)',
'ʕ•́ᴥ•̀ʔっ',
'(ㆆ_ㆆ)',
].pickRandomly()!;
].randomlyPicked!;
static final String magicWord = <String>[
'to be over the rainbow!',
'to infinity and beyond!',
'to see the future.',
].pickRandomly()!;
].randomlyPicked!;
static final String errorMessage = 'Something went wrong...$sadFace';
static final String loginErrorMessage =

View File

@ -3,7 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/item/item.dart';
import 'package:hacki/repositories/hacker_news_repository.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/styles/dimens.dart';
final GoRouter router = GoRouter(
observers: <NavigatorObserver>[
@ -25,6 +29,34 @@ final GoRouter router = GoRouter(
return ItemScreen.phone(args);
},
),
GoRoute(
path: '${ItemScreen.routeName}/:itemId',
builder: (BuildContext context, GoRouterState state) {
final String? itemIdStr = state.pathParameters['itemId'];
final int? itemId = itemIdStr?.itemId;
if (itemId == null) {
throw GoError("item id can't be null");
}
return FutureBuilder<Item?>(
future: locator.get<HackerNewsRepository>().fetchItem(id: itemId),
builder: (BuildContext context, AsyncSnapshot<Item?> snapshot) {
if (snapshot.hasData) {
final ItemScreenArgs args =
ItemScreenArgs(item: snapshot.data!);
return ItemScreen.phone(args);
} else {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(
strokeWidth: Dimens.pt2,
),
),
);
}
},
);
},
),
GoRoute(
path: LogScreen.routeName,
builder: (_, __) => const LogScreen(),

View File

@ -499,7 +499,7 @@ class CommentsCubit extends Cubit<CommentsState> with Loggable {
? state.comments.elementAt(e.index - 1)
: null,
)
.whereNotNull()
.nonNulls
.toList();
if (onScreenComments.isEmpty && state.comments.isNotEmpty) {
@ -520,7 +520,7 @@ class CommentsCubit extends Cubit<CommentsState> with Loggable {
final int firstVisibleRootCommentIndex =
state.comments.indexOf(firstVisibleRootComment);
startIndex = min(firstVisibleRootCommentIndex + 1, totalComments);
} else {
} else if (onScreenComments.isNotEmpty) {
final int lastVisibleCommentIndex =
state.comments.indexOf(onScreenComments.last);
startIndex = min(lastVisibleCommentIndex + 1, totalComments);
@ -557,7 +557,7 @@ class CommentsCubit extends Cubit<CommentsState> with Loggable {
? state.comments.elementAt(e.index - 1)
: null,
)
.whereNotNull()
.nonNulls
.toList();
/// The index of first comment visible on screen.

View File

@ -52,9 +52,9 @@ class PreferenceState extends Equatable {
bool get isComplexStoryTileEnabled => _isOn<DisplayModePreference>();
bool get isFaviconEnabled => _isOn<FaviconModePreference>();
bool get isDividerEnabled => _isOn<DividerPreference>();
bool get isEyeCandyEnabled => _isOn<EyeCandyModePreference>();
bool get isFaviconEnabled => _isOn<FaviconModePreference>();
bool get isReaderEnabled => _isOn<ReaderModePreference>();

View File

@ -129,9 +129,11 @@ mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
}
if (linkToShare != null) {
await Share.share(
linkToShare,
sharePositionOrigin: rect,
await SharePlus.instance.share(
ShareParams(
uri: Uri.parse(linkToShare),
sharePositionOrigin: rect,
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'dart:math';
extension ListExtension<T> on List<T> {
T? pickRandomly() {
T? get randomlyPicked {
if (isEmpty) return null;
final Random random = Random(DateTime.now().millisecondsSinceEpoch);
final int luckyNumber = random.nextInt(length);

View File

@ -5,7 +5,6 @@ import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@ -54,9 +53,7 @@ Future<void> main({bool testing = false}) async {
Hive.init(tempPath);
final HydratedStorage storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
storageDirectory: HydratedStorageDirectory(tempPath),
);
HydratedBloc.storage = storage;
@ -289,6 +286,7 @@ class HackiApp extends StatelessWidget {
brightness:
isDarkModeEnabled ? Brightness.dark : Brightness.light,
seedColor: state.appColor,
dynamicSchemeVariant: DynamicSchemeVariant.fidelity,
);
return FeatureDiscovery(
child: MediaQuery(
@ -314,15 +312,16 @@ class HackiApp extends StatelessWidget {
? Palette.black
: null,
dividerTheme: DividerThemeData(
color: Palette.grey.withOpacity(0.2),
color: Palette.grey.withValues(alpha: 0.2),
),
switchTheme: SwitchThemeData(
trackColor: WidgetStateProperty.resolveWith(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return colorScheme.primary.withOpacity(0.6);
return colorScheme.primary
.withValues(alpha: 0.6);
} else {
return Palette.grey.withOpacity(0.2);
return Palette.grey.withValues(alpha: 0.2);
}
},
),
@ -345,13 +344,13 @@ class HackiApp extends StatelessWidget {
color: (isDarkModeEnabled
? Palette.white
: Palette.black)
.withOpacity(0.4),
.withValues(alpha: 0.4),
),
),
),
sliderTheme: SliderThemeData(
inactiveTrackColor:
colorScheme.primary.withOpacity(0.5),
colorScheme.primary.withValues(alpha: 0.5),
activeTrackColor: colorScheme.primary,
thumbColor: colorScheme.primary,
),

View File

@ -0,0 +1,12 @@
enum MaxOfflineStoriesCount {
ten(20, '20'),
fifty(50, '50'),
hundred(100, '100'),
twoHundred(200, '200'),
all(null, 'All');
const MaxOfflineStoriesCount(this.count, this.label);
final int? count;
final String label;
}

View File

@ -8,6 +8,7 @@ export 'font.dart';
export 'font_size.dart';
export 'hacker_news_data_source.dart';
export 'item/item.dart';
export 'max_offline_stories_count.dart';
export 'post_data.dart';
export 'preference.dart';
export 'search_params.dart';

View File

@ -37,6 +37,7 @@ abstract final class Preference<T> extends Equatable with SettingsDisplayable {
const FaviconModePreference(),
const MetadataModePreference(),
const StoryUrlModePreference(),
const DividerPreference(),
/// Divider.
const MarkReadStoriesModePreference(),
@ -50,7 +51,6 @@ abstract final class Preference<T> extends Equatable with SettingsDisplayable {
const ManualPaginationPreference(),
const SwipeGesturePreference(),
const HapticFeedbackPreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
const DevMode(),
],
@ -270,6 +270,27 @@ final class StoryUrlModePreference extends BooleanPreference {
String get subtitle => '''show url in story tile.''';
}
final class DividerPreference extends BooleanPreference {
const DividerPreference({bool? val})
: super(val: val ?? _dividerDefaultValue);
static const bool _dividerDefaultValue = true;
@override
DividerPreference copyWith({required bool? val}) {
return DividerPreference(val: val);
}
@override
String get key => 'dividerPreference';
@override
String get title => 'Divider';
@override
String get subtitle => '''show divider between story tiles.''';
}
final class ReaderModePreference extends BooleanPreference {
const ReaderModePreference({bool? val})
: super(val: val ?? _readerModeDefaultValue);
@ -316,27 +337,6 @@ final class MarkReadStoriesModePreference extends BooleanPreference {
String get subtitle => 'grey out stories you have read.';
}
final class EyeCandyModePreference extends BooleanPreference {
const EyeCandyModePreference({bool? val})
: super(val: val ?? _eyeCandyModeDefaultValue);
static const bool _eyeCandyModeDefaultValue = false;
@override
EyeCandyModePreference copyWith({required bool? val}) {
return EyeCandyModePreference(val: val);
}
@override
String get key => 'eyeCandyMode';
@override
String get title => 'Eye Candy';
@override
String get subtitle => 'some sort of magic.';
}
final class ManualPaginationPreference extends BooleanPreference {
const ManualPaginationPreference({bool? val})
: super(val: val ?? _paginationModeDefaultValue);

View File

@ -175,7 +175,8 @@ class HackerNewsWebRepository with Loggable {
subtextElement.querySelector(_ageSelector) ??
subtextElement.querySelector('.age');
final String? dateStr = postDateElement?.attributes['title'];
final String? dateStr =
postDateElement?.attributes['title']?.split(' ').firstOrNull;
final int? timestamp = dateStr == null
? null
: DateTime.parse(dateStr)
@ -278,7 +279,7 @@ class HackerNewsWebRepository with Loggable {
final List<Element> elements =
document.querySelectorAll(_aThingSelector);
final Iterable<int> parsedIds =
elements.map((Element e) => int.tryParse(e.id)).whereNotNull();
elements.map((Element e) => int.tryParse(e.id)).nonNulls;
return parsedIds;
} on DioException catch (e) {
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
@ -401,7 +402,8 @@ class HackerNewsWebRepository with Loggable {
/// Get comment age.
final Element? cmtAgeElement =
element.querySelector(_commentAgeSelector);
final String? ageString = cmtAgeElement?.attributes['title'];
final String? ageString =
cmtAgeElement?.attributes['title']?.split(' ').firstOrNull;
final int? timestamp = ageString == null
? null

View File

@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/utils/debouncer.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
@ -402,10 +404,20 @@ class PreferenceRepository with Loggable {
);
}
final List<String> _storiesIdQueue = <String>[];
final Debouncer _debouncer = Debouncer(delay: AppDurations.tenSeconds);
Future<void> addHasRead(int storyId) async {
final String key = _getHasReadKey(storyId);
if (Platform.isIOS) {
await _syncedPrefs.setBool(key: key, val: true);
_storiesIdQueue.add(key);
_debouncer.run(() {
for (final String key in _storiesIdQueue) {
_syncedPrefs.setBool(key: key, val: true);
}
_storiesIdQueue.clear();
});
} else {
final SharedPreferences prefs = await _prefs;

View File

@ -7,7 +7,6 @@ import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
/// [SembastRepository] is for storing stories and comments for faster loading.

View File

@ -36,7 +36,8 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin, RouteAware, ItemActionMixin, Loggable {
late final TabController tabController;
late final StreamSubscription<String> intentDataStreamSubscription;
late final StreamSubscription<List<SharedMediaFile>>
intentDataStreamSubscription;
late final StreamSubscription<String?> notificationStreamSubscription;
late final StreamSubscription<String?> siriSuggestionStreamSubscription;
@ -59,10 +60,13 @@ class _HomeScreenState extends State<HomeScreen>
void initState() {
super.initState();
ReceiveSharingIntent.getInitialText().then(onShareExtensionTapped);
ReceiveSharingIntent.instance
.getInitialMedia()
.then(onShareExtensionTapped);
intentDataStreamSubscription =
ReceiveSharingIntent.getTextStream().listen(onShareExtensionTapped);
intentDataStreamSubscription = ReceiveSharingIntent.instance
.getMediaStream()
.listen(onShareExtensionTapped);
if (!selectNotificationSubject.hasListener) {
notificationStreamSubscription =
@ -113,7 +117,8 @@ class _HomeScreenState extends State<HomeScreen>
previous.isComplexStoryTileEnabled !=
current.isComplexStoryTileEnabled ||
previous.isMetadataEnabled != current.isMetadataEnabled ||
previous.isSwipeGestureEnabled != current.isSwipeGestureEnabled,
previous.isSwipeGestureEnabled != current.isSwipeGestureEnabled ||
previous.isDividerEnabled != current.isDividerEnabled,
builder: (BuildContext context, PreferenceState preferenceState) {
return DefaultTabController(
length: tabLength,
@ -222,12 +227,12 @@ class _HomeScreenState extends State<HomeScreen>
}
}
void onShareExtensionTapped(String? event) {
void onShareExtensionTapped(List<SharedMediaFile>? event) {
logInfo('share intent received: $event');
if (event == null) return;
final int? id = event.itemId;
final int? id = event.firstOrNull?.path.itemId;
if (id != null) {
locator.get<HackerNewsRepository>().fetchItem(id: id).then((Item? item) {

View File

@ -45,8 +45,9 @@ class PinnedStories extends StatelessWidget {
],
),
child: ColoredBox(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.2),
color: Theme.of(context).colorScheme.primary.withValues(
alpha: 0.2,
),
child: StoryTile(
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story,
@ -59,11 +60,15 @@ class PinnedStories extends StatelessWidget {
),
),
),
if (state.pinnedStories.isNotEmpty)
if (state.pinnedStories.isNotEmpty &&
!preferenceState.isDividerEnabled)
Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Divider(
color: Theme.of(context).colorScheme.primary.withOpacity(0.8),
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.8),
),
),
],

View File

@ -309,7 +309,7 @@ class _ItemScreenState extends State<ItemScreen>
context: context,
backgroundColor: Theme.of(context)
.canvasColor
.withOpacity(0.6),
.withValues(alpha: 0.6),
foregroundColor:
Theme.of(context).iconTheme.color,
item: widget.item,
@ -351,7 +351,7 @@ class _ItemScreenState extends State<ItemScreen>
appBar: CustomAppBar(
context: context,
backgroundColor:
Theme.of(context).canvasColor.withOpacity(0.6),
Theme.of(context).canvasColor.withValues(alpha: 0.6),
foregroundColor: Theme.of(context).iconTheme.color,
item: widget.item,
onFontSizeTap: onFontSizeTapped,

View File

@ -33,9 +33,9 @@ class CustomFloatingActionButton extends StatelessWidget {
CustomDescribedFeatureOverlay(
feature: DiscoverableFeature.jumpUpButton,
contentLocation: ContentLocation.above,
tapTarget: const Icon(
tapTarget: Icon(
Icons.keyboard_arrow_up,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
child: InkWell(
onLongPress: () =>
@ -59,9 +59,9 @@ class CustomFloatingActionButton extends StatelessWidget {
),
CustomDescribedFeatureOverlay(
feature: DiscoverableFeature.jumpDownButton,
tapTarget: const Icon(
tapTarget: Icon(
Icons.keyboard_arrow_down,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
child: InkWell(
onLongPress: () {

View File

@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/discoverable_feature.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
class FavIconButton extends StatelessWidget {
@ -24,7 +23,7 @@ class FavIconButton extends StatelessWidget {
icon: CustomDescribedFeatureOverlay(
tapTarget: Icon(
isFav ? Icons.favorite : Icons.favorite_border,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
feature: DiscoverableFeature.addStoryToFavList,
child: Icon(

View File

@ -22,9 +22,9 @@ class InThreadSearchIconButton extends StatelessWidget {
transitionType: ContainerTransitionType.fadeThrough,
closedBuilder: (BuildContext context, void Function() action) {
return CustomDescribedFeatureOverlay(
tapTarget: const Icon(
tapTarget: Icon(
Icons.search,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
feature: DiscoverableFeature.searchInThread,
child: IconButton(

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/models/discoverable_feature.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
class LinkIconButton extends StatelessWidget {
@ -17,13 +16,13 @@ class LinkIconButton extends StatelessWidget {
Widget build(BuildContext context) {
return IconButton(
tooltip: 'Open this story in browser',
icon: const CustomDescribedFeatureOverlay(
icon: CustomDescribedFeatureOverlay(
tapTarget: Icon(
Icons.stream,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
feature: DiscoverableFeature.openStoryInWebView,
child: Icon(
child: const Icon(
Icons.stream,
),
),

View File

@ -6,7 +6,6 @@ import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/context_extension.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
class PinIconButton extends StatelessWidget {
@ -31,7 +30,7 @@ class PinIconButton extends StatelessWidget {
icon: CustomDescribedFeatureOverlay(
tapTarget: Icon(
pinned ? Icons.push_pin : Icons.push_pin_outlined,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
feature: DiscoverableFeature.pinToTop,
child: Icon(

View File

@ -245,6 +245,8 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
hintStyle: TextStyle(
color: Palette.grey,
),
enabledBorder: InputBorder.none,
disabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
border: InputBorder.none,
),

View File

@ -17,7 +17,9 @@ class LogScreen extends StatelessWidget {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Theme.of(context).canvasColor.withOpacity(0.6),
backgroundColor: Theme.of(context).canvasColor.withValues(
alpha: 0.6,
),
elevation: 0,
actions: <Widget>[
if (snapshot.data != null)
@ -38,10 +40,12 @@ class LogScreen extends StatelessWidget {
),
],
),
body: ListView(
children: <Widget>[
...?snapshot.data?.map(Text.new),
],
body: Scrollbar(
child: ListView(
children: <Widget>[
...?snapshot.data?.map(Text.new),
],
),
),
);
},

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/styles/styles.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
class QrCodeScannerScreen extends StatefulWidget {
const QrCodeScannerScreen({super.key});
@ -64,10 +64,4 @@ class _QrCodeScannerScreenState extends State<QrCodeScannerScreen> {
}
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}

View File

@ -88,7 +88,7 @@ class InboxView extends StatelessWidget {
child: LinearProgressIndicator(
color: Theme.of(context)
.primaryColor
.withOpacity(0.1),
.withValues(alpha: 0.1),
),
),
FadeIn(
@ -130,10 +130,11 @@ class InboxView extends StatelessWidget {
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(
unreadCommentsIds.contains(e.id)
? 1
: 0.6,
.withValues(
alpha:
unreadCommentsIds.contains(e.id)
? 1
: 0.6,
),
),
maxLines: 4,

View File

@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/haptic_feedback_util.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class OfflineListTile extends StatelessWidget {
@ -94,41 +96,48 @@ class OfflineListTile extends StatelessWidget {
}
});
} else {
context.read<StoriesBloc>().add(ClearMaxOfflineStoriesCount());
Connectivity()
.checkConnectivity()
.then((List<ConnectivityResult> res) {
if (!res.contains(ConnectivityResult.none) && context.mounted) {
showDialog<bool>(
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Download web pages as well?'),
content: const Text('It will take longer time.'),
actions: <Widget>[
TextButton(
onPressed: () => context.pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => context.pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => context.pop(true),
child: const Text('Yes'),
),
],
),
).then((bool? includeWebPage) {
if (includeWebPage != null) {
WakelockPlus.enable();
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBoxes.pt12,
const Text(
'How many stories do you want to download?',
),
for (final MaxOfflineStoriesCount count
in MaxOfflineStoriesCount.values)
ListTile(
title: Text(count.label),
onTap: () {
HapticFeedbackUtil.selection();
if (context.mounted) {
context.read<StoriesBloc>().add(
StoriesDownload(includingWebPage: includeWebPage),
);
}
}
});
context.pop();
final StoriesBloc storiesBloc =
context.read<StoriesBloc>()
..add(
UpdateMaxOfflineStoriesCount(
count: count,
),
);
showConfirmationDialog(
context,
storiesBloc,
);
},
),
],
),
);
},
);
}
});
}
@ -137,4 +146,38 @@ class OfflineListTile extends StatelessWidget {
},
);
}
void showConfirmationDialog(BuildContext context, StoriesBloc storiesBloc) {
showDialog<bool>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Download web pages as well?'),
content: const Text('It will take longer time.'),
actions: <Widget>[
TextButton(
onPressed: () => context.pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => context.pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => context.pop(true),
child: const Text('Yes'),
),
],
),
).then((bool? includeWebPage) {
if (includeWebPage != null) {
WakelockPlus.enable();
storiesBloc.add(
StoriesDownload(
includingWebPage: includeWebPage,
),
);
}
});
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@ -62,6 +63,12 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
child: Column(
children: <Widget>[
ListTile(
leading: Icon(
Icons.person,
color: widget.authState.isLoggedIn
? Theme.of(context).colorScheme.primary
: null,
),
title: Text(
widget.authState.isLoggedIn ? 'Log Out' : 'Log In',
),
@ -221,9 +228,6 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
bool isInProgress,
) {
return DropdownMenu<HackerNewsDataSource>(
/// Make sure no stories are being fetched
/// before switching data source.
enabled: !isInProgress,
initialSelection: preferenceState.dataSource,
dropdownMenuEntries:
HackerNewsDataSource.values
@ -343,7 +347,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
),
const Divider(),
],
if (preference is StoryUrlModePreference) const Divider(),
if (preference is DividerPreference) const Divider(),
],
ListTile(
title: const Text(
@ -395,7 +399,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
),
onTap: showClearCacheDialog,
),
if (preferenceState.isDevModeEnabled)
if (preferenceState.isDevModeEnabled) ...<Widget>[
ListTile(
title: const Text(
'Logs',
@ -404,6 +408,20 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
context.go(Paths.log.landing);
},
),
ListTile(
title: const Text(
'Reset Feature Discovery',
),
onTap: () {
HapticFeedbackUtil.light();
FeatureDiscovery.clearPreferences(
context,
DiscoverableFeature.values
.map((DiscoverableFeature f) => f.featureId),
);
},
),
],
ListTile(
title: const Text('About'),
subtitle: const Text('nothing interesting here.'),
@ -831,10 +849,12 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
try {
final File originalFile = await LogUtil.exportLog();
final XFile file = XFile(originalFile.path);
final ShareResult result = await Share.shareXFiles(
<XFile>[file],
subject: 'hacki_log',
sharePositionOrigin: rect,
final ShareResult result = await SharePlus.instance.share(
ShareParams(
files: <XFile>[file],
subject: 'hacki_log',
sharePositionOrigin: rect,
),
);
if (result.status == ShareResultStatus.success) {

View File

@ -75,11 +75,6 @@ class CommentTile extends StatelessWidget {
final Color primaryColor = Theme.of(context).colorScheme.primary;
final Brightness brightness = Theme.of(context).brightness;
final Color color = _getColor(
level,
primaryColor: primaryColor,
brightness: brightness,
);
final Widget child = DeviceGestureWrapper(
child: Column(
@ -224,7 +219,7 @@ class CommentTile extends StatelessWidget {
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.8),
.withValues(alpha: 0.8),
)
else if (comment.hidden)
const CenteredText.hidden()
@ -307,12 +302,7 @@ class CommentTile extends StatelessWidget {
),
);
final double commentBackgroundColorOpacity =
Theme.of(context).canvasColor != Palette.white ? 0.03 : 0.15;
final Color commentColor = prefState.isEyeCandyEnabled
? color.withOpacity(commentBackgroundColorOpacity)
: Palette.transparent;
const Color commentColor = Palette.transparent;
final bool isMyComment = comment.deleted == false &&
context.read<AuthBloc>().state.username == comment.by;
@ -322,7 +312,9 @@ class CommentTile extends StatelessWidget {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
color: Theme.of(context).colorScheme.primary.withValues(
alpha: 0.2,
),
),
child: wrapper,
);
@ -349,13 +341,30 @@ class CommentTile extends StatelessWidget {
)
: null,
color: shouldHighlight
? primaryColor.withOpacity(0.2)
? primaryColor.withValues(alpha: 0.2)
: commentColor,
),
child: wrapper,
);
}
if (<int>[0, 1, 2, 3].contains(level)) {
wrapper = Stack(
children: <Widget>[
wrapper,
Positioned(
left: Dimens.zero,
top: Dimens.zero,
bottom: Dimens.zero,
width: Dimens.pt24,
child: Container(
color: Colors.transparent,
),
),
],
);
}
return wrapper;
},
),
@ -387,7 +396,7 @@ class CommentTile extends StatelessWidget {
}
final double opacity = ((10 - level) / 10).clamp(0.3, 1);
final Color color = primaryColor.withOpacity(opacity);
final Color color = primaryColor.withValues(alpha: opacity);
levelToBorderColors[cacheKey] = color;
return color;

View File

@ -390,7 +390,7 @@ TextSpan buildTextSpan(
return TextSpan(
text: element.text,
style: style?.copyWith(
backgroundColor: primaryColor.withOpacity(0.3),
backgroundColor: primaryColor.withValues(alpha: 0.3),
),
);
} else if (element is EmphasisElement) {

View File

@ -84,10 +84,10 @@ class _CustomTabBarState extends State<CustomTabBar> {
Tab(
child: CustomDescribedFeatureOverlay(
onComplete: showOnboarding,
tapTarget: const Icon(
tapTarget: Icon(
Icons.person,
size: TextDimens.pt16,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
feature: DiscoverableFeature.login,
child: BlocBuilder<NotificationCubit, NotificationState>(

View File

@ -69,8 +69,9 @@ class _DownloadProgressReminderState extends State<DownloadProgressReminder>
const Spacer(),
LinearProgressIndicator(
value: progress,
color:
Theme.of(context).colorScheme.primary.withOpacity(0.5),
color: Theme.of(context).colorScheme.primary.withValues(
alpha: 0.5,
),
),
],
),

View File

@ -20,6 +20,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
required this.items,
required this.onTap,
required this.refreshController,
this.showDivider = false,
super.key,
this.showAuthor = true,
this.useSimpleTileForStory = false,
@ -38,6 +39,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
});
final bool showAuthor;
final bool showDivider;
final bool useSimpleTileForStory;
final bool showWebPreviewOnStoryTile;
final bool showMetadataOnStoryTile;
@ -78,6 +80,21 @@ class ItemsListView<T extends Item> extends StatelessWidget {
final bool swipeGestureEnabled =
context.read<PreferenceCubit>().state.isSwipeGestureEnabled;
return <Widget>[
if (showDivider)
Padding(
padding: EdgeInsetsGeometry.only(
bottom:
showWebPreviewOnStoryTile ? Dimens.pt8 : Dimens.zero,
),
child: const Divider(
height: Dimens.zero,
),
)
else if (context.read<SplitViewCubit>().state.enabled)
const Divider(
height: Dimens.pt6,
color: Palette.transparent,
),
if (useSimpleTileForStory)
FadeIn(
child: InkWell(
@ -134,7 +151,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
),
),
)
else
else ...<Widget>[
GestureDetector(
/// If swipe gesture is enabled on home screen, use long press
/// instead of slide action to trigger the action menu.
@ -154,15 +171,11 @@ class ItemsListView<T extends Item> extends StatelessWidget {
),
),
),
if (useSimpleTileForStory || !showWebPreviewOnStoryTile)
const Divider(
height: Dimens.zero,
)
else if (context.read<SplitViewCubit>().state.enabled)
const Divider(
height: Dimens.pt6,
color: Palette.transparent,
),
if (showDivider && showWebPreviewOnStoryTile)
const SizedBox(
height: Dimens.pt8,
),
],
];
} else if (e is Comment) {
return <Widget>[

View File

@ -29,7 +29,7 @@ class OfflineBanner extends StatelessWidget {
textAlign: showExitButton ? TextAlign.left : TextAlign.center,
),
backgroundColor:
Theme.of(context).colorScheme.primary.withOpacity(0.3),
Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
actions: <Widget>[
if (showExitButton)
TextButton(

View File

@ -52,7 +52,8 @@ class _StoriesListViewState extends State<StoriesListView>
current.isComplexStoryTileEnabled ||
previous.isMetadataEnabled != current.isMetadataEnabled ||
previous.isManualPaginationEnabled !=
current.isManualPaginationEnabled,
current.isManualPaginationEnabled ||
previous.isDividerEnabled != current.isDividerEnabled,
builder: (BuildContext context, PreferenceState preferenceState) {
return BlocConsumer<StoriesBloc, StoriesState>(
listenWhen: (StoriesState previous, StoriesState current) =>
@ -76,6 +77,7 @@ class _StoriesListViewState extends State<StoriesListView>
current.statusByType[widget.storyType]),
builder: (BuildContext context, StoriesState state) {
return ItemsListView<Story>(
showDivider: preferenceState.isDividerEnabled,
showOfflineBanner: true,
markReadStories: preferenceState.isMarkReadStoriesEnabled,
showWebPreviewOnStoryTile:

View File

@ -126,9 +126,9 @@ class StoryTile extends StatelessWidget {
return Semantics(
label: story.screenReaderLabel,
excludeSemantics: true,
child: InkWell(
child: TapDownWrapper(
onTap: onTap,
onDoubleTap: () {
onLongPress: () {
if (story.url.isNotEmpty) {
LinkUtil.launch(
story.url,
@ -272,7 +272,7 @@ class _LinkPreviewPlaceholder extends StatelessWidget {
child: Shimmer.fromColors(
baseColor: Theme.of(context).colorScheme.primary,
highlightColor:
Theme.of(context).colorScheme.primary.withOpacity(0.8),
Theme.of(context).colorScheme.primary.withValues(alpha: 0.8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[

View File

@ -6,9 +6,11 @@ class TapDownWrapper extends StatefulWidget {
required this.child,
super.key,
this.onTap,
this.onLongPress,
});
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final Widget child;
@override
@ -37,6 +39,7 @@ class _TapDownWrapperState extends State<TapDownWrapper>
onTapDown: onTapDown,
onTapUp: onTapUp,
onTapCancel: onTapCancel,
onLongPress: widget.onLongPress,
behavior: HitTestBehavior.opaque,
child: AnimatedBuilder(
animation:

View File

@ -502,7 +502,9 @@ ${info.toJson()}
final String? desc =
_getMetaContent(document, 'property', 'og:description');
if (desc != null &&
!desc.contains('JavaScript is disabled in your browser')) return desc;
!desc.contains('JavaScript is disabled in your browser')) {
return desc;
}
final String? description =
_getMetaContent(document, 'name', 'description') ??

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:hacki/styles/dimens.dart';
final class SizedBoxes {
static const SizedBox pt2 = SizedBox(
height: Dimens.pt2,
width: Dimens.pt2,
);
static const SizedBox pt4 = SizedBox(
height: Dimens.pt4,
width: Dimens.pt4,
);
static const SizedBox pt6 = SizedBox(
height: Dimens.pt6,
width: Dimens.pt6,
);
static const SizedBox pt8 = SizedBox(
height: Dimens.pt8,
width: Dimens.pt8,
);
static const SizedBox pt12 = SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
);
static const SizedBox pt16 = SizedBox(
height: Dimens.pt16,
width: Dimens.pt16,
);
static const SizedBox pt18 = SizedBox(
height: Dimens.pt18,
width: Dimens.pt18,
);
static const SizedBox pt20 = SizedBox(
height: Dimens.pt20,
width: Dimens.pt20,
);
static const SizedBox pt24 = SizedBox(
height: Dimens.pt24,
width: Dimens.pt24,
);
static const SizedBox pt36 = SizedBox(
height: Dimens.pt36,
width: Dimens.pt36,
);
static const SizedBox pt48 = SizedBox(
height: Dimens.pt48,
width: Dimens.pt48,
);
}

View File

@ -1,4 +1,5 @@
export 'dimens.dart';
export 'media_query.dart';
export 'palette.dart';
export 'sized_boxes.dart';
export 'theme.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
extension ThemeDataExtension on ThemeData {
Color get readGrey => colorScheme.onSurface.withOpacity(0.6);
Color get readGrey => colorScheme.onSurface.withValues(alpha: 0.6);
Color get metadataColor => colorScheme.onSurface.withOpacity(0.8);
Color get metadataColor => colorScheme.onSurface.withValues(alpha: 0.8);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,75 @@
name: hacki
description: A Hacker News reader.
version: 2.9.4+152
version: 2.11.0+159
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.24.3"
flutter: "3.32.5"
dependencies:
adaptive_theme: ^3.2.0
animations: ^2.0.8
badges: ^3.0.2
bloc: ^8.1.1
bloc_concurrency: ^0.2.5
bloc: ^9.0.0
bloc_concurrency: ^0.3.0
cached_network_image: ^3.3.0
collection: ^1.17.1
collection: ^1.19.0
connectivity_plus: ^6.0.3
device_info_plus: ^10.1.0
device_info_plus: ^11.2.1
dio: ^5.7.0
dio_smart_retry: ^6.0.0
dio_smart_retry: ^7.0.1
equatable: ^2.0.5
fast_gbk: ^1.0.0
feature_discovery:
git:
url: https://github.com/livinglist/feature_discovery
ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
feature_discovery: ^0.14.2
flutter:
sdk: flutter
flutter_bloc: ^8.1.5
flutter_bloc: ^9.0.0
flutter_cache_manager: ^3.3.2
flutter_email_sender: ^6.0.3
flutter_email_sender: ^7.0.0
flutter_fadein: ^2.0.0
flutter_feather_icons: 2.0.0+1
flutter_inappwebview: ^6.0.0
flutter_local_notifications: ^17.1.2
flutter_inappwebview: ^6.1.5
flutter_local_notifications: ^19.3.0
flutter_material_color_picker: ^1.2.0
flutter_native_splash: ^2.4.1
flutter_secure_storage: ^9.2.2
flutter_slidable: ^3.0.0
flutter_native_splash: ^2.4.4
flutter_secure_storage: ^9.2.4
flutter_slidable:
git:
url: https://github.com/TimeFinderApp/flutter_slidable
ref: 6ab5a79f1f8f984ad5b5733fd94e90c8f97a0c8d
font_awesome_flutter: ^10.3.0
get_it: ^7.7.0
go_router: ^14.1.4
get_it: ^8.0.3
go_router: ^15.2.0
hive: ^2.2.3
html: ^0.15.1
html_unescape: ^2.0.0
http: ^1.1.0
hydrated_bloc: ^9.1.5
hydrated_bloc: ^10.0.0
in_app_review:
path: components/in_app_review
intl: ^0.19.0
intl: ^0.20.1
linkify: ^5.0.0
logger: ^2.4.0
memoize: ^3.0.0
package_info_plus: ^8.0.0
path: ^1.8.2
path_provider: ^2.1.3
path_provider_android: ^2.2.5
path_provider_foundation: ^2.4.0
pretty_dio_logger: ^1.3.1
path: ^1.9.0
path_provider: ^2.1.5
path_provider_android: ^2.2.15
path_provider_foundation: ^2.4.1
pretty_dio_logger: ^1.4.0
pull_to_refresh:
git:
url: https://github.com/livinglist/flutter_pulltorefresh
ref: master
qr_code_scanner: ^1.0.1
qr_code_scanner_plus: ^2.0.10+1
qr_flutter: ^4.1.0
receive_sharing_intent: 1.5.4
receive_sharing_intent: 1.8.1
responsive_builder: ^0.7.0
rxdart: ^0.27.7
rxdart: ^0.28.0
scrollable_positioned_list: ^0.3.5
sembast: ^3.7.1
share_plus: ^9.0.0
share_plus: ^11.0.0
shared_preferences: ^2.2.3
shared_preferences_android: ^2.2.3
shared_preferences_foundation: ^2.4.0
@ -81,10 +81,13 @@ dependencies:
visibility_detector: ^0.4.0+2
wakelock_plus: ^1.2.5
webview_flutter: ^4.8.0
workmanager: ^0.5.1
workmanager: ^0.6.0
dependency_overrides:
web: ^1.0.0
dev_dependencies:
bloc_test: ^9.1.0
bloc_test: ^10.0.0
flutter_driver:
sdk: flutter
flutter_test:
@ -92,7 +95,7 @@ dev_dependencies:
integration_test:
sdk: flutter
mocktail: ^1.0.0
very_good_analysis: ^5.0.0
very_good_analysis: ^9.0.0
flutter:
uses-material-design: true

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:args/args.dart';
import 'package:dio/dio.dart';
@ -30,11 +31,18 @@ Again, if the only thing a reporter had to do was read the report to find the fa
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
};
const int itemId = 11536543;
final Dio dio = Dio();
final Dio dio = Dio(BaseOptions(validateStatus: (_) => true));
final Uri url = Uri.parse('$itemBaseUrl$itemId');
final Options option = Options(headers: headers, persistentConnection: true);
final Response<String> response =
await dio.getUri<String>(url, options: option);
final Response<String> response = await dio.getUri<String>(
url,
options: option,
);
if (response.statusCode != HttpStatus.ok) {
print('Status code: ${response.statusCode}');
return;
}
/// Parse the HTML and select all the comment elements.
final String data = response.data ?? '';
@ -49,12 +57,14 @@ Again, if the only thing a reporter had to do was read the report to find the fa
const String commentTextSelector =
'''td > table > tbody > tr > td.default > div.comment > div.commtext''';
final Element? cmtTextElement = e.querySelector(commentTextSelector);
final String parsedText =
await parseCommentTextHtml(cmtTextElement?.innerHtml ?? '');
final String parsedText = await parseCommentTextHtml(
cmtTextElement?.innerHtml ?? '',
);
if (parsedText != text) {
final Uri url =
Uri.parse('https://api.github.com/repos/livinglist/hacki/issues');
final Uri url = Uri.parse(
'https://api.github.com/repos/livinglist/hacki/issues',
);
const String issueTitle = 'Parser check failed.';
/// Check if an issue with same title already exists.
@ -82,9 +92,7 @@ Again, if the only thing a reporter had to do was read the report to find the fa
await dio.postUri<String>(
url,
data: githubIssuePayload,
options: Options(
headers: githubHeaders,
),
options: Options(headers: githubHeaders),
);
print('Issue created.');
}
@ -101,24 +109,15 @@ Future<String> parseCommentTextHtml(String text) async {
return HtmlUnescape()
.convert(text)
.replaceAllMapped(
RegExp(
r'\<div class="reply"\>(.*?)\<\/div\>',
dotAll: true,
),
RegExp(r'\<div class="reply"\>(.*?)\<\/div\>', dotAll: true),
(Match match) => '',
)
.replaceAllMapped(
RegExp(
r'\<span class="(.*?)"\>(.*?)\<\/span\>',
dotAll: true,
),
RegExp(r'\<span class="(.*?)"\>(.*?)\<\/span\>', dotAll: true),
(Match match) => '${match[2]}',
)
.replaceAllMapped(
RegExp(
r'\<p\>(.*?)\<\/p\>',
dotAll: true,
),
RegExp(r'\<p\>(.*?)\<\/p\>', dotAll: true),
(Match match) => '\n\n${match[1]}',
)
.replaceAllMapped(