mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
fda0b8ede5 | |||
1dc1cce12b | |||
1c87d741cb | |||
d3b01b97fd | |||
f0413f99f0 | |||
2c213dee58 | |||
1652de4c2d | |||
df807a4a11 | |||
4f7a515490 | |||
341e04d645 | |||
872c4359d4 | |||
de1eac31da | |||
0dab102904 | |||
9c616eb734 | |||
691a0cb2ac | |||
6612227249 | |||
78e022f3cb | |||
677b9d4b7d | |||
cc55913022 | |||
08973bb829 | |||
fdce94f2e7 | |||
0897abf27e | |||
f07254dbd4 | |||
1408b7343a | |||
bedc3b66ec | |||
3e3941380d | |||
bbed4e0e75 | |||
a4ae6a20e1 | |||
3413b1686d |
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.
|
||||
|
@ -19,6 +19,7 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
PreferenceRepository? preferenceRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
HackerNewsWebRepository? hackerNewsWebRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
}) : _authBloc = authBloc,
|
||||
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||
_preferenceRepository =
|
||||
@ -27,6 +28,8 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_hackerNewsWebRepository =
|
||||
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(FavState.init()) {
|
||||
init();
|
||||
}
|
||||
@ -36,8 +39,9 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final HackerNewsWebRepository _hackerNewsWebRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
late final StreamSubscription<String>? _usernameSubscription;
|
||||
static const int _pageSize = 20;
|
||||
static const int _pageSize = 100;
|
||||
|
||||
Future<void> init() async {
|
||||
_usernameSubscription = _authBloc.stream
|
||||
@ -55,6 +59,8 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
_hackerNewsRepository
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
getFromCache: (int id) =>
|
||||
_sembastRepository.getCachedItem(id: id),
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
@ -97,7 +103,10 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
void removeFav(int id) {
|
||||
_preferenceRepository
|
||||
..removeFav(username: username, id: id)
|
||||
..removeFav(username: '', id: id);
|
||||
..removeFav(
|
||||
username: '',
|
||||
id: id,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -200,6 +209,7 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
}
|
||||
|
||||
void _onItemLoaded(Item item) {
|
||||
_sembastRepository.cacheItem(item);
|
||||
emit(
|
||||
state.copyWith(
|
||||
favItems: List<Item>.from(state.favItems)..add(item),
|
||||
@ -207,6 +217,9 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
);
|
||||
}
|
||||
|
||||
void switchTab() =>
|
||||
emit(state.copyWith(isDisplayingStories: !state.isDisplayingStories));
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_usernameSubscription?.cancel();
|
||||
|
@ -7,6 +7,7 @@ class FavState extends Equatable {
|
||||
required this.status,
|
||||
required this.mergeStatus,
|
||||
required this.currentPage,
|
||||
required this.isDisplayingStories,
|
||||
});
|
||||
|
||||
FavState.init()
|
||||
@ -14,13 +15,21 @@ class FavState extends Equatable {
|
||||
favItems = <Item>[],
|
||||
status = Status.idle,
|
||||
mergeStatus = Status.idle,
|
||||
currentPage = 0;
|
||||
currentPage = 0,
|
||||
isDisplayingStories = true;
|
||||
|
||||
final List<int> favIds;
|
||||
final List<Item> favItems;
|
||||
final Status status;
|
||||
final Status mergeStatus;
|
||||
final int currentPage;
|
||||
final bool isDisplayingStories;
|
||||
|
||||
List<Comment> get favComments =>
|
||||
favItems.whereType<Comment>().toList(growable: false);
|
||||
|
||||
List<Story> get favStories =>
|
||||
favItems.whereType<Story>().toList(growable: false);
|
||||
|
||||
FavState copyWith({
|
||||
List<int>? favIds,
|
||||
@ -28,6 +37,7 @@ class FavState extends Equatable {
|
||||
Status? status,
|
||||
Status? mergeStatus,
|
||||
int? currentPage,
|
||||
bool? isDisplayingStories,
|
||||
}) {
|
||||
return FavState(
|
||||
favIds: favIds ?? this.favIds,
|
||||
@ -35,6 +45,7 @@ class FavState extends Equatable {
|
||||
status: status ?? this.status,
|
||||
mergeStatus: mergeStatus ?? this.mergeStatus,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
isDisplayingStories: isDisplayingStories ?? this.isDisplayingStories,
|
||||
);
|
||||
}
|
||||
|
||||
@ -45,5 +56,6 @@ class FavState extends Equatable {
|
||||
currentPage,
|
||||
favIds,
|
||||
favItems,
|
||||
isDisplayingStories,
|
||||
];
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -41,6 +41,7 @@ class BuildableComment extends Comment with Buildable {
|
||||
BuildableComment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
int? kid,
|
||||
}) {
|
||||
return BuildableComment(
|
||||
id: id,
|
||||
@ -49,7 +50,7 @@ class BuildableComment extends Comment with Buildable {
|
||||
score: score,
|
||||
by: by,
|
||||
text: text,
|
||||
kids: kids,
|
||||
kids: kid == null ? kids : <int>[...kids, kid],
|
||||
dead: dead,
|
||||
deleted: deleted,
|
||||
hidden: hidden ?? this.hidden,
|
||||
|
@ -36,6 +36,7 @@ class Comment extends Item {
|
||||
Comment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
int? kid,
|
||||
}) {
|
||||
return Comment(
|
||||
id: id,
|
||||
@ -44,7 +45,7 @@ class Comment extends Item {
|
||||
score: score,
|
||||
by: by,
|
||||
text: text,
|
||||
kids: kids,
|
||||
kids: kid == null ? kids : <int>[...kids, kid],
|
||||
dead: dead,
|
||||
deleted: deleted,
|
||||
hidden: hidden ?? this.hidden,
|
||||
|
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(),
|
||||
],
|
||||
@ -110,11 +110,11 @@ final class SwipeGesturePreference extends BooleanPreference {
|
||||
String get key => 'swipeGestureMode';
|
||||
|
||||
@override
|
||||
String get title => 'Swipe Gesture';
|
||||
String get title => 'Swipe Gesture for Switching Tabs';
|
||||
|
||||
@override
|
||||
String get subtitle =>
|
||||
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
|
||||
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu and double tap to open the url (if complex tile is disabled).''';
|
||||
}
|
||||
|
||||
final class NotificationModePreference extends BooleanPreference {
|
||||
@ -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);
|
||||
|
@ -302,24 +302,32 @@ class HackerNewsRepository with Loggable {
|
||||
|
||||
/// Fetch a list of [Item] based on ids and return results
|
||||
/// using a stream.
|
||||
Stream<Item> fetchItemsStream({required List<int> ids}) async* {
|
||||
Stream<Item> fetchItemsStream({
|
||||
required List<int> ids,
|
||||
Future<Item?> Function(int)? getFromCache,
|
||||
}) async* {
|
||||
for (final int id in ids) {
|
||||
final Item? item =
|
||||
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
|
||||
if (json == null) return null;
|
||||
final Item? cachedItem = await getFromCache?.call(id);
|
||||
if (cachedItem != null) {
|
||||
yield cachedItem;
|
||||
} else {
|
||||
final Item? item =
|
||||
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
|
||||
if (json == null) return null;
|
||||
|
||||
if (json.isStory) {
|
||||
final Story story = Story.fromJson(json);
|
||||
return story;
|
||||
} else if (json.isComment) {
|
||||
final Comment comment = Comment.fromJson(json);
|
||||
return comment;
|
||||
if (json.isStory) {
|
||||
final Story story = Story.fromJson(json);
|
||||
return story;
|
||||
} else if (json.isComment) {
|
||||
final Comment comment = Comment.fromJson(json);
|
||||
return comment;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (item != null) {
|
||||
yield item;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (item != null) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -1,15 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
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.
|
||||
/// This is currently used by [TimeMachineCubit], [NotificationCubit] and
|
||||
/// [FavCubit].
|
||||
///
|
||||
/// Sembast [Database] is used as its database and is being stored in the
|
||||
/// documents directory assigned by host system which you can retrieve
|
||||
@ -67,7 +69,7 @@ class SembastRepository with Loggable {
|
||||
return db;
|
||||
}
|
||||
|
||||
//#region Cached comments for time machine feature.
|
||||
//#region Cached comments for time machine feature and favorites screen.
|
||||
Future<Map<String, Object?>> cacheComment(Comment comment) async {
|
||||
final Database db = _database ?? await initializeDatabase();
|
||||
final StoreRef<int, Map<String, Object?>> store =
|
||||
@ -89,7 +91,34 @@ class SembastRepository with Loggable {
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> deleteAllCachedComments() async {
|
||||
Future<Map<String, Object?>> cacheItem(Item item) async {
|
||||
final Database db = _database ?? await initializeDatabase();
|
||||
final StoreRef<int, Map<String, Object?>> store =
|
||||
intMapStoreFactory.store(_cachedCommentsKey);
|
||||
return store.record(item.id).put(db, item.toJson());
|
||||
}
|
||||
|
||||
Future<Item?> getCachedItem({required int id}) async {
|
||||
final Database db = _database ?? await initializeDatabase();
|
||||
final StoreRef<int, Map<String, Object?>> store =
|
||||
intMapStoreFactory.store(_cachedCommentsKey);
|
||||
final RecordSnapshot<int, Map<String, Object?>>? snapshot =
|
||||
await store.record(id).getSnapshot(db);
|
||||
if (snapshot != null) {
|
||||
final bool isStory = snapshot['type'] == 'story';
|
||||
if (isStory) {
|
||||
final Story story = Story.fromJson(snapshot.value);
|
||||
return story;
|
||||
} else {
|
||||
final Comment comment = Comment.fromJson(snapshot.value);
|
||||
return comment;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> deleteAllCachedItems() async {
|
||||
final Database db = _database ?? await initializeDatabase();
|
||||
final StoreRef<int, Map<String, Object?>> store =
|
||||
intMapStoreFactory.store(_cachedCommentsKey);
|
||||
|
@ -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,5 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
@ -130,124 +128,12 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
top: Dimens.pt50,
|
||||
child: Visibility(
|
||||
visible: pageType == PageType.fav,
|
||||
child: BlocConsumer<FavCubit, FavState>(
|
||||
listener: (BuildContext context, FavState favState) {
|
||||
if (favState.status == Status.success) {
|
||||
refreshControllerFav
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
}
|
||||
},
|
||||
buildWhen: (FavState previous, FavState current) =>
|
||||
previous.favItems.length != current.favItems.length,
|
||||
builder: (BuildContext context, FavState favState) {
|
||||
Widget? header() => authState.isLoggedIn
|
||||
? BlocSelector<FavCubit, FavState, Status>(
|
||||
selector: (FavState state) => state.mergeStatus,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
Status status,
|
||||
) {
|
||||
return TextButton(
|
||||
onPressed: () =>
|
||||
context.read<FavCubit>().merge(
|
||||
onError: (AppException e) =>
|
||||
showErrorSnackBar(e.message),
|
||||
onSuccess: () => showSnackBar(
|
||||
content: '''Sync completed.''',
|
||||
),
|
||||
),
|
||||
child: status == Status.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child:
|
||||
CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text('Sync from Hacker News'),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null;
|
||||
|
||||
if (favState.favItems.isEmpty &&
|
||||
favState.status != Status.inProgress) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content:
|
||||
'Your favorite stories will show up here.'
|
||||
'\nThey will be synced to your Hacker '
|
||||
'News account if you are logged in.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.isComplexStoryTileEnabled !=
|
||||
current.isComplexStoryTileEnabled ||
|
||||
previous.isMetadataEnabled !=
|
||||
current.isMetadataEnabled ||
|
||||
previous.isUrlEnabled != current.isUrlEnabled,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return ItemsListView<Item>(
|
||||
showWebPreviewOnStoryTile:
|
||||
prefState.isComplexStoryTileEnabled,
|
||||
showMetadataOnStoryTile:
|
||||
prefState.isMetadataEnabled,
|
||||
showFavicon: prefState.isFaviconEnabled,
|
||||
showUrl: prefState.isUrlEnabled,
|
||||
useSimpleTileForStory: true,
|
||||
refreshController: refreshControllerFav,
|
||||
items: favState.favItems,
|
||||
onRefresh: () {
|
||||
HapticFeedbackUtil.light();
|
||||
context.read<FavCubit>().refresh();
|
||||
},
|
||||
onLoadMore: () {
|
||||
context.read<FavCubit>().loadMore();
|
||||
},
|
||||
onTap: (Item item) => goToItemScreen(
|
||||
args: ItemScreenArgs(item: item),
|
||||
),
|
||||
header: header(),
|
||||
itemBuilder: (Widget child, Item item) {
|
||||
return Slidable(
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
context
|
||||
.read<FavCubit>()
|
||||
.removeFav(item.id);
|
||||
},
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.close,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: FavoritesScreen(
|
||||
refreshController: refreshControllerFav,
|
||||
authState: authState,
|
||||
onItemTap: (Item item) => goToItemScreen(
|
||||
args: ItemScreenArgs(item: item),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
187
lib/screens/profile/widgets/favorites_screen.dart
Normal file
187
lib/screens/profile/widgets/favorites_screen.dart
Normal file
@ -0,0 +1,187 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/profile/widgets/centered_message_view.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
class FavoritesScreen extends StatelessWidget {
|
||||
const FavoritesScreen({
|
||||
required this.refreshController,
|
||||
required this.authState,
|
||||
required this.onItemTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final RefreshController refreshController;
|
||||
final AuthState authState;
|
||||
final void Function(Item) onItemTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<FavCubit, FavState>(
|
||||
listener: (BuildContext context, FavState favState) {
|
||||
if (favState.status == Status.success) {
|
||||
refreshController
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
}
|
||||
},
|
||||
buildWhen: (FavState previous, FavState current) =>
|
||||
previous.favItems.length != current.favItems.length ||
|
||||
previous.isDisplayingStories != current.isDisplayingStories,
|
||||
builder: (BuildContext context, FavState favState) {
|
||||
Widget? header() => Column(
|
||||
children: <Widget>[
|
||||
if (authState.isLoggedIn)
|
||||
BlocSelector<FavCubit, FavState, Status>(
|
||||
selector: (FavState state) => state.mergeStatus,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
Status status,
|
||||
) {
|
||||
return TextButton(
|
||||
onPressed: () => context.read<FavCubit>().merge(
|
||||
onError: (AppException e) =>
|
||||
context.showErrorSnackBar(e.message),
|
||||
onSuccess: () => context.showSnackBar(
|
||||
content: '''Sync completed.''',
|
||||
),
|
||||
),
|
||||
child: status == Status.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Sync from Hacker News',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
selected: favState.isDisplayingStories,
|
||||
label: 'Story',
|
||||
onSelected: (_) => context.read<FavCubit>().switchTab(),
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
selected: !favState.isDisplayingStories,
|
||||
label: 'Comment',
|
||||
onSelected: (_) => context.read<FavCubit>().switchTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (favState.favItems.isEmpty && favState.status != Status.inProgress) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content: 'Your favorite stories will show up here.'
|
||||
'\nThey will be synced to your Hacker '
|
||||
'News account if you are logged in.',
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
if (favState.isDisplayingStories && favState.favStories.isEmpty) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content: 'No favorite story.',
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (!favState.isDisplayingStories &&
|
||||
favState.favComments.isEmpty) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content: 'No favorite comment.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.isComplexStoryTileEnabled !=
|
||||
current.isComplexStoryTileEnabled ||
|
||||
previous.isMetadataEnabled != current.isMetadataEnabled ||
|
||||
previous.isUrlEnabled != current.isUrlEnabled,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return ItemsListView<Item>(
|
||||
showWebPreviewOnStoryTile: prefState.isComplexStoryTileEnabled,
|
||||
showMetadataOnStoryTile: prefState.isMetadataEnabled,
|
||||
showFavicon: prefState.isFaviconEnabled,
|
||||
showUrl: prefState.isUrlEnabled,
|
||||
useSimpleTileForStory: true,
|
||||
refreshController: refreshController,
|
||||
items: favState.isDisplayingStories
|
||||
? favState.favStories
|
||||
: favState.favComments,
|
||||
onRefresh: () {
|
||||
HapticFeedbackUtil.light();
|
||||
context.read<FavCubit>().refresh();
|
||||
},
|
||||
onLoadMore: () {
|
||||
context.read<FavCubit>().loadMore();
|
||||
},
|
||||
onTap: onItemTap,
|
||||
header: header(),
|
||||
itemBuilder: (Widget child, Item item) {
|
||||
return Slidable(
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
context.read<FavCubit>().removeFav(item.id);
|
||||
},
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.close,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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.'),
|
||||
@ -606,7 +624,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
|
||||
context.pop();
|
||||
locator
|
||||
.get<SembastRepository>()
|
||||
.deleteAllCachedComments()
|
||||
.deleteAllCachedItems()
|
||||
.whenComplete(
|
||||
locator.get<OfflineRepository>().deleteAll,
|
||||
)
|
||||
@ -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) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
export 'centered_message_view.dart';
|
||||
export 'enter_offline_mode_list_tile.dart';
|
||||
export 'favorites_screen.dart';
|
||||
export 'inbox_view.dart';
|
||||
export 'offline_list_tile.dart';
|
||||
export 'settings.dart';
|
||||
|
@ -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,10 +80,32 @@ 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(
|
||||
onTap: () => onTap(e),
|
||||
|
||||
/// If swipe gesture is enabled on home screen, use
|
||||
/// long press instead of slide action to trigger
|
||||
/// the action menu.
|
||||
onLongPress: swipeGestureEnabled
|
||||
? () => onMoreTapped?.call(e, context.rect)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: Dimens.pt8,
|
||||
@ -127,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.
|
||||
@ -147,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>[
|
||||
|
@ -123,20 +123,41 @@ class LinkView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: imageUri ?? Constants.favicon(url),
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
cacheKey: imageUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
if (url.isEmpty) {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
: () {
|
||||
if (imageUri?.isNotEmpty ?? false) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: imageUri!,
|
||||
fit:
|
||||
isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
cacheKey: imageUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
if (url.isEmpty) {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Constants.favicon(url),
|
||||
fit: BoxFit.scaleDown,
|
||||
cacheKey: iconUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
return const FadeIn(
|
||||
child: Icon(
|
||||
Icons.public,
|
||||
size: Dimens.pt20,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (url.isNotEmpty) {
|
||||
return Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Constants.favicon(url),
|
||||
@ -152,8 +173,16 @@ class LinkView extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
} else {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -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,7 +126,7 @@ class StoryTile extends StatelessWidget {
|
||||
return Semantics(
|
||||
label: story.screenReaderLabel,
|
||||
excludeSemantics: true,
|
||||
child: InkWell(
|
||||
child: TapDownWrapper(
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
if (story.url.isNotEmpty) {
|
||||
@ -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:
|
||||
|
@ -3,7 +3,18 @@ import 'package:hacki/models/models.dart' show Comment;
|
||||
class CommentCache {
|
||||
static final Map<int, Comment> _comments = <int, Comment>{};
|
||||
|
||||
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
||||
void cacheComment(Comment comment) {
|
||||
_comments[comment.id] = comment;
|
||||
|
||||
/// Comments fetched from `HackerNewsWebRepository` doesn't have populated
|
||||
/// `kids` field, this is why we need to update that of the parent
|
||||
/// comment here.
|
||||
final int parentId = comment.parent;
|
||||
final Comment? parent = _comments[parentId];
|
||||
if (parent == null || parent.kids.contains(comment.id)) return;
|
||||
final Comment updatedParent = parent.copyWith(kid: comment.id);
|
||||
_comments[parentId] = updatedParent;
|
||||
}
|
||||
|
||||
Comment? getComment(int id) => _comments[id];
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user