mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
28 Commits
v2.9.4
...
jf/android
Author | SHA1 | Date | |
---|---|---|---|
7747aa6b0e | |||
fda0b8ede5 | |||
bb51b3b0cb | |||
3ff8e82966 | |||
1dc1cce12b | |||
1c87d741cb | |||
d3b01b97fd | |||
f0413f99f0 | |||
2c213dee58 | |||
1652de4c2d | |||
df807a4a11 | |||
4f7a515490 | |||
341e04d645 | |||
872c4359d4 | |||
de1eac31da | |||
0dab102904 | |||
9c616eb734 | |||
691a0cb2ac | |||
6612227249 | |||
78e022f3cb | |||
677b9d4b7d | |||
cc55913022 | |||
08973bb829 | |||
fdce94f2e7 | |||
0897abf27e | |||
f07254dbd4 | |||
1408b7343a | |||
bedc3b66ec |
4
.github/workflows/publish_ios.yml
vendored
4
.github/workflows/publish_ios.yml
vendored
@ -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
1
.gitignore
vendored
@ -44,3 +44,4 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
/android/build/reports
|
||||
|
@ -1 +1 @@
|
||||
2.7.5
|
||||
3.3.0
|
||||
|
@ -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]
|
||||
|
@ -1,3 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin.jvm.target.validation.mode = IGNORE
|
@ -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
|
||||
|
@ -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"
|
@ -25,6 +25,8 @@ apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace "dev.britannio.in_app_review"
|
||||
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 */;
|
||||
}
|
||||
|
@ -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>
|
@ -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">
|
||||
|
@ -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>
|
@ -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>
|
24
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
24
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal 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
|
||||
}
|
@ -8,6 +8,8 @@
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
6
ios/StoryWidget/Assets.xcassets/Contents.json
Normal file
6
ios/StoryWidget/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
36
ios/StoryWidget/Extensions/ArrayExtension.swift
Normal file
36
ios/StoryWidget/Extensions/ArrayExtension.swift
Normal 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
|
||||
}
|
||||
}
|
9
ios/StoryWidget/Extensions/Date+TimeAgoString.swift
Normal file
9
ios/StoryWidget/Extensions/Date+TimeAgoString.swift
Normal 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())
|
||||
}
|
||||
}
|
10
ios/StoryWidget/Extensions/Int+OrZero.swift
Normal file
10
ios/StoryWidget/Extensions/Int+OrZero.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
extension Int? {
|
||||
var orZero: Int {
|
||||
guard let unwrapped = self else {
|
||||
return 0
|
||||
}
|
||||
return unwrapped
|
||||
}
|
||||
}
|
88
ios/StoryWidget/Extensions/StringExtension.swift
Normal file
88
ios/StoryWidget/Extensions/StringExtension.swift
Normal 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
|
||||
}
|
||||
}
|
11
ios/StoryWidget/Info.plist
Normal file
11
ios/StoryWidget/Info.plist
Normal 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>
|
54
ios/StoryWidget/Models/Comment.swift
Normal file
54
ios/StoryWidget/Models/Comment.swift
Normal 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)
|
||||
}
|
||||
}
|
59
ios/StoryWidget/Models/Item.swift
Normal file
59
ios/StoryWidget/Models/Item.swift
Normal 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
|
||||
}
|
||||
}
|
48
ios/StoryWidget/Models/SearchFilter.swift
Normal file
48
ios/StoryWidget/Models/SearchFilter.swift
Normal 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
|
||||
}
|
||||
}
|
62
ios/StoryWidget/Models/SearchParams.swift
Normal file
62
ios/StoryWidget/Models/SearchParams.swift
Normal 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
|
||||
}
|
||||
}
|
98
ios/StoryWidget/Models/Story.swift
Normal file
98
ios/StoryWidget/Models/Story.swift
Normal 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
|
||||
)
|
||||
}
|
56
ios/StoryWidget/Models/StoryType.swift
Normal file
56
ios/StoryWidget/Models/StoryType.swift
Normal 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"
|
||||
]
|
||||
}
|
53
ios/StoryWidget/Models/User.swift
Normal file
53
ios/StoryWidget/Models/User.swift
Normal 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)
|
||||
}
|
||||
}
|
17
ios/StoryWidget/SelectStoryTypeIntent.swift
Normal file
17
ios/StoryWidget/SelectStoryTypeIntent.swift
Normal 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
|
||||
}
|
||||
}
|
14
ios/StoryWidget/StoryEntry.swift
Normal file
14
ios/StoryWidget/StoryEntry.swift
Normal 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
|
||||
)
|
||||
}
|
130
ios/StoryWidget/StoryRepository.swift
Normal file
130
ios/StoryWidget/StoryRepository.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
21
ios/StoryWidget/StorySource.swift
Normal file
21
ios/StoryWidget/StorySource.swift
Normal 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"
|
||||
]
|
||||
}
|
42
ios/StoryWidget/StoryTimelineProvider.swift
Normal file
42
ios/StoryWidget/StoryTimelineProvider.swift
Normal 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
|
||||
}
|
||||
}
|
84
ios/StoryWidget/StoryWidget.swift
Normal file
84
ios/StoryWidget/StoryWidget.swift
Normal 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.")
|
||||
}
|
||||
}
|
9
ios/StoryWidget/StoryWidgetBundle.swift
Normal file
9
ios/StoryWidget/StoryWidgetBundle.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct StoryWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
StoryWidget()
|
||||
}
|
||||
}
|
8
ios/StoryWidget/Timeline+Placeholder.swift
Normal file
8
ios/StoryWidget/Timeline+Placeholder.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import WidgetKit
|
||||
|
||||
extension Timeline where EntryType == StoryEntry {
|
||||
static let errorPlaceholder: Timeline<StoryEntry> = .init(
|
||||
entries: [.errorPlaceholder],
|
||||
policy: .atEnd
|
||||
)
|
||||
}
|
18
ios/Widget/SelectStoryTypeIntent.swift
Normal file
18
ios/Widget/SelectStoryTypeIntent.swift
Normal 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
|
||||
}
|
||||
}
|
21
ios/Widget/StorySource.swift
Normal file
21
ios/Widget/StorySource.swift
Normal 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"
|
||||
]
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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});
|
||||
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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(),
|
||||
|
@ -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.
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
),
|
||||
|
12
lib/models/max_offline_stories_count.dart
Normal file
12
lib/models/max_offline_stories_count.dart
Normal 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;
|
||||
}
|
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -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: () {
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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>(
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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>[
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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>[
|
||||
|
@ -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:
|
||||
|
@ -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') ??
|
||||
|
49
lib/styles/sized_boxes.dart
Normal file
49
lib/styles/sized_boxes.dart
Normal 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,
|
||||
);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export 'dimens.dart';
|
||||
export 'media_query.dart';
|
||||
export 'palette.dart';
|
||||
export 'sized_boxes.dart';
|
||||
export 'theme.dart';
|
||||
|
@ -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);
|
||||
}
|
||||
|
659
pubspec.lock
659
pubspec.lock
File diff suppressed because it is too large
Load Diff
71
pubspec.yaml
71
pubspec.yaml
@ -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
|
||||
|
@ -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(
|
||||
|
Submodule submodules/flutter updated: 2663184aa7...fcf2c11572
Reference in New Issue
Block a user