Compare commits
64 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 | |||
c24670d5d8 | |||
a50c456390 | |||
915eb47ab6 | |||
c442a5d2e7 | |||
fbedf327ee | |||
45c684b774 | |||
b6015ae6ca | |||
b240dccc8e | |||
949562a34a | |||
9d8af331c7 | |||
031ff7519d | |||
62bab9d781 | |||
b9ff92a27b | |||
0332cd531d | |||
b76c5dd64c | |||
7325a08002 | |||
78bb1c6a6c | |||
c34ffe22da | |||
a621dc0291 | |||
88a12d3339 | |||
50d4cdfad9 | |||
366a461c96 | |||
3f1e9d0fff | |||
d09c10b3f8 | |||
fd5730e189 | |||
c9cc6a5df0 | |||
8d4b232097 | |||
8af643e584 | |||
70a56f4ade | |||
c685f33f99 | |||
518608893d | |||
856efa7c14 | |||
d1957ffb82 | |||
553a37961d | |||
bade5b4356 |
6
.github/workflows/commit_check.yml
vendored
@ -3,8 +3,10 @@ name: Commit Guard
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- '!master'
|
||||
pull_request:
|
||||
# Run on any TARGET branches.
|
||||
branches: [ '**' ]
|
||||
|
||||
jobs:
|
||||
commit_check:
|
||||
@ -21,4 +23,4 @@ jobs:
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
- run: submodules/flutter/bin/flutter analyze lib test integration_test
|
||||
- run: submodules/flutter/bin/flutter test
|
||||
- run: submodules/flutter/bin/flutter test
|
||||
|
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
@ -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]
|
||||
|
BIN
android/app/src/main/res/drawable-night-v21/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
BIN
android/app/src/main/res/drawable-night/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -1,12 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
BIN
android/app/src/main/res/drawable/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -1,12 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
19
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
19
android/app/src/main/res/values-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
@ -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"
|
15
assets/remote-config-dev.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"athingComtrSelector": "#hnmain > tbody > tr > td > table > tbody > .athing.comtr",
|
||||
"commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext",
|
||||
"commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a",
|
||||
"commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age",
|
||||
"commentIndentSelector": "td > table > tbody > tr > td.ind",
|
||||
"storySelector": "#hnmain > tbody > tr > td > table > tbody > .athing",
|
||||
"subtextSelector": "#hnmain > tbody > tr > td > table > tbody > tr > .subtext",
|
||||
"titlelineSelector": ".title > .titleline > a",
|
||||
"pointSelector": ".subline > .score",
|
||||
"userSelector": ".subline > .hnuser",
|
||||
"ageSelector": ".subline > .age",
|
||||
"cmtCountSelector": ".subline > a",
|
||||
"moreLinkSelector": "#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a"
|
||||
}
|
@ -3,5 +3,13 @@
|
||||
"commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext",
|
||||
"commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a",
|
||||
"commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age",
|
||||
"commentIndentSelector": "td > table > tbody > tr > td.ind"
|
||||
"commentIndentSelector": "td > table > tbody > tr > td.ind",
|
||||
"storySelector": "#hnmain > tbody > tr > td > table > tbody > .athing",
|
||||
"subtextSelector": "#hnmain > tbody > tr > td > table > tbody > tr > .subtext",
|
||||
"titlelineSelector": ".title > .titleline > a",
|
||||
"pointSelector": ".subline > .score",
|
||||
"userSelector": ".subline > .hnuser",
|
||||
"ageSelector": ".subline > .age",
|
||||
"cmtCountSelector": ".subline > a",
|
||||
"moreLinkSelector": "#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a"
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace "dev.britannio.in_app_review"
|
||||
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
|
@ -5,103 +5,116 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
version: "2.11.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.18.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
lints:
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.15.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
version: "1.9.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -111,58 +124,66 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.8"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "0.7.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
sdks:
|
||||
dart: ">=2.16.2 <3.0.0"
|
||||
flutter: ">=2.5.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
1
fastlane/metadata/android/en-US/changelogs/148.txt
Normal file
@ -0,0 +1 @@
|
||||
- UX improvements.
|
1
fastlane/metadata/android/en-US/changelogs/149.txt
Normal file
@ -0,0 +1 @@
|
||||
- Improved tablet mode, you can now resize submission panel.
|
@ -60,7 +60,7 @@ void main() {
|
||||
expect(firstStoryFinder, findsOneWidget);
|
||||
|
||||
await tester.tap(firstStoryFinder);
|
||||
await tester.pump(const Duration(seconds: 4));
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
},
|
||||
reportKey: 'scrolling_timeline',
|
||||
);
|
||||
|
@ -1,19 +1,79 @@
|
||||
//
|
||||
// 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] = []
|
||||
let urlContentType = kUTTypeURL as String
|
||||
var sharedMedia: [SharedMediaFile] = []
|
||||
let urlContentType = UTType.url
|
||||
@IBOutlet weak var imageView: UIImageView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
@ -32,20 +92,18 @@ class ActionViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
|
||||
attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in
|
||||
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.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://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,2 +1,3 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
|
@ -1,2 +1,4 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
|
||||
|
@ -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,3 +1,6 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
|
100
ios/Podfile.lock
@ -1,7 +1,6 @@
|
||||
PODS:
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
@ -10,73 +9,74 @@ 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_secure_storage (6.0.0):
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_siri_suggestions (0.0.1):
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- in_app_review (0.2.0):
|
||||
- Flutter
|
||||
- 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):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- wakelock_plus (0.0.1):
|
||||
- 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`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- 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 (from `.symlinks/plugins/wakelock/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/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:
|
||||
@ -97,10 +97,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_siri_suggestions:
|
||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
integration_test:
|
||||
@ -109,53 +109,53 @@ 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:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
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_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
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: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
|
||||
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: 0957b955069bb512c22bae4cadad9f4c34161dbe
|
||||
PODFILE CHECKSUM: f03c7c11cf2b623592c89c68c628682778bb78b4
|
||||
|
||||
COCOAPODS: 1.13.0
|
||||
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 */
|
||||
@ -231,11 +347,11 @@
|
||||
buildPhases = (
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
@ -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;
|
||||
@ -387,7 +541,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||
};
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */ = {
|
||||
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 */
|
||||
@ -537,6 +722,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@ -548,7 +734,8 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@ -561,24 +748,23 @@
|
||||
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;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -619,6 +805,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@ -636,7 +823,8 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@ -674,6 +862,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@ -685,7 +874,8 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@ -700,24 +890,23 @@
|
||||
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;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -733,26 +922,25 @@
|
||||
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;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -776,20 +964,20 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = "";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
@ -817,7 +1005,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -825,13 +1013,13 @@
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = "";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -856,20 +1044,20 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = "";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -895,20 +1083,20 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = "";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
@ -938,7 +1126,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -946,13 +1134,13 @@
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = "";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -979,20 +1167,20 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = "";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = "";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -1004,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 */
|
||||
@ -1047,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
@ -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
|
||||
}
|
@ -6,7 +6,7 @@ import flutter_secure_storage
|
||||
import path_provider_foundation
|
||||
import flutter_local_notifications
|
||||
|
||||
@UIApplicationMain
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
@ -22,7 +22,7 @@ import flutter_local_notifications
|
||||
WorkmanagerPlugin.registerTask(withIdentifier: "workmanager.background.task")
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
|
||||
}
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*15))
|
||||
|
||||
|
22
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "background.png",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "darkbackground.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
After Width: | Height: | Size: 69 B |
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
vendored
Normal file
After Width: | Height: | Size: 69 B |
@ -1,23 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 69 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 69 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 69 B |
@ -16,13 +16,19 @@
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
@ -33,5 +39,6 @@
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
@ -1,84 +1,88 @@
|
||||
<?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>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>workmanager.background.task</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Hacki</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>hacki</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:example.com</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>workmanager.background.task</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Hacki</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>hacki</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>14.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:example.com</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>AppGroupId</key>
|
||||
<string>group.com.jiaqi.hacki</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -8,6 +8,8 @@
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
@ -3,186 +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 = kUTTypeImage as String
|
||||
let videoContentType = kUTTypeMovie as String
|
||||
let textContentType = kUTTypeText as String
|
||||
let urlContentType = kUTTypeURL as String
|
||||
let fileURLType = kUTTypeFileURL as String;
|
||||
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) {
|
||||
handleImages(content: content, attachment: attachment, index: index)
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
|
||||
handleText(content: content, attachment: attachment, index: index)
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(fileURLType) {
|
||||
handleFiles(content: content, attachment: attachment, index: index)
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
|
||||
handleUrl(content: content, attachment: attachment, index: index)
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(videoContentType) {
|
||||
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, options: nil) { [weak self] data, error in
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, 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, 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, options: nil) { [weak self] data, error in
|
||||
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)
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
} else {
|
||||
let selectorOpenURL = sel_registerName("openURL:")
|
||||
|
||||
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)
|
||||
while (responder != nil) {
|
||||
if (responder?.responds(to: selectorOpenURL))! {
|
||||
_ = responder?.perform(selectorOpenURL, with: url)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
|
||||
attachment.loadItem(forTypeIdentifier: fileURLType, 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()
|
||||
responder = responder!.next
|
||||
}
|
||||
}
|
||||
|
||||
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
private func dismissWithError() {
|
||||
@ -198,57 +288,38 @@ class ShareViewController: SLComposeServiceViewController {
|
||||
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
private func redirectToHostApp(type: RedirectType) {
|
||||
let url = URL(string: "ShareMedia://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)
|
||||
@ -261,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
|
||||
@ -276,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: \(self.thumbnail)\n\tduration: \(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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
|
||||
)
|
||||
|
||||
|
@ -3,40 +3,41 @@ import 'dart:math';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
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> {
|
||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
|
||||
StoriesBloc({
|
||||
required PreferenceCubit preferenceCubit,
|
||||
required FilterCubit filterCubit,
|
||||
OfflineRepository? offlineRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
HackerNewsWebRepository? hackerNewsWebRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceCubit = preferenceCubit,
|
||||
_filterCubit = filterCubit,
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_hackerNewsRepository =
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_hackerNewsWebRepository =
|
||||
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
|
||||
_preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const StoriesState.init()) {
|
||||
on<LoadStories>(
|
||||
onLoadStories,
|
||||
transformer: sequential(),
|
||||
transformer: concurrent(),
|
||||
);
|
||||
on<StoriesInitialize>(onInitialize);
|
||||
on<StoriesRefresh>(onRefresh);
|
||||
@ -52,53 +53,55 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
on<StoryDownloaded>(onStoryDownloaded);
|
||||
on<StoriesEnterOfflineMode>(onEnterOfflineMode);
|
||||
on<StoriesExitOfflineMode>(onExitOfflineMode);
|
||||
on<StoriesPageSizeChanged>(onPageSizeChanged);
|
||||
on<ClearAllReadStories>(onClearAllReadStories);
|
||||
on<UpdateMaxOfflineStoriesCount>(onUpdateMaxOfflineStoriesCount);
|
||||
on<ClearMaxOfflineStoriesCount>(onClearMaxOfflineStoriesCount);
|
||||
|
||||
_preferenceSubscription = _preferenceCubit.stream
|
||||
.distinct((PreferenceState lhs, PreferenceState rhs) {
|
||||
return lhs.dataSource == rhs.dataSource;
|
||||
}).listen((PreferenceState prefState) {
|
||||
add(StoriesInitialize());
|
||||
});
|
||||
}
|
||||
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final FilterCubit _filterCubit;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final HackerNewsWebRepository _hackerNewsWebRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
DeviceScreenType? deviceScreenType;
|
||||
StreamSubscription<PreferenceState>? _preferenceSubscription;
|
||||
static const int _smallPageSize = 10;
|
||||
static const int _largePageSize = 20;
|
||||
static const int _tabletSmallPageSize = 15;
|
||||
static const int _tabletLargePageSize = 25;
|
||||
static const int _pageSize = 30;
|
||||
|
||||
Future<void> onInitialize(
|
||||
StoriesInitialize event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
_preferenceSubscription ??= _preferenceCubit.stream
|
||||
.distinct((PreferenceState previous, PreferenceState next) {
|
||||
return previous.isComplexStoryTileEnabled ==
|
||||
next.isComplexStoryTileEnabled;
|
||||
})
|
||||
//.debounceTime(AppDurations.twoSeconds)
|
||||
.listen((PreferenceState event) {
|
||||
final bool isComplexTile = event.isComplexStoryTileEnabled;
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
final HackerNewsDataSource dataSource = _preferenceCubit.state.dataSource;
|
||||
logInfo('data source: $dataSource');
|
||||
|
||||
if (pageSize != state.currentPageSize) {
|
||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||
}
|
||||
});
|
||||
final bool isComplexTile = _preferenceCubit.state.isComplexStoryTileEnabled;
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
const StoriesState.init().copyWith(
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
storiesToBeDownloaded: state.storiesToBeDownloaded,
|
||||
isOfflineReading: state.isOfflineReading,
|
||||
dataSource: dataSource,
|
||||
),
|
||||
);
|
||||
|
||||
if (event.startup) {
|
||||
final List<ConnectivityResult> status =
|
||||
await Connectivity().checkConnectivity();
|
||||
logInfo('network status: $status');
|
||||
if (status.contains(ConnectivityResult.none)) {
|
||||
logInfo('no network connection, entering offline mode.');
|
||||
add(StoriesEnterOfflineMode());
|
||||
}
|
||||
}
|
||||
|
||||
for (final StoryType type in _preferenceCubit.state.tabs) {
|
||||
add(LoadStories(type: type));
|
||||
}
|
||||
@ -108,41 +111,70 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
LoadStories event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
if (state.dataSource == null) {
|
||||
logError('data source should not be null.');
|
||||
}
|
||||
|
||||
final StoryType type = event.type;
|
||||
if (state.isOfflineReading) {
|
||||
logInfo('($type) loading stories from local cache.');
|
||||
final List<int> ids =
|
||||
await _offlineRepository.getCachedStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 1)
|
||||
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
||||
);
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
ids: ids.sublist(0, min(_pageSize, ids.length)),
|
||||
)
|
||||
.listen((Story story) => add(StoryLoaded(story: story, type: type)))
|
||||
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
||||
} else {
|
||||
} else if (event.useApi || state.dataSource == HackerNewsDataSource.api) {
|
||||
logInfo('($type) loading stories from API.');
|
||||
final List<int> ids =
|
||||
await _hackerNewsRepository.fetchStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 1)
|
||||
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
||||
);
|
||||
|
||||
await _hackerNewsRepository
|
||||
.fetchStoriesStream(
|
||||
ids: ids.sublist(0, state.currentPageSize),
|
||||
sequential: _preferenceCubit.state.isComplexStoryTileEnabled ||
|
||||
_preferenceCubit.state.isFaviconEnabled,
|
||||
ids: ids.sublist(0, min(_pageSize, ids.length)),
|
||||
sequential: true,
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).asFuture<void>();
|
||||
add(StoryLoadingCompleted(type: type));
|
||||
} else {
|
||||
logInfo('($type) loading stories from web.');
|
||||
emit(
|
||||
state
|
||||
.copyWithCurrentPageUpdated(type: type, to: 1)
|
||||
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
||||
);
|
||||
|
||||
await _hackerNewsWebRepository
|
||||
.fetchStoriesStream(event.type, page: 1)
|
||||
.handleError((dynamic e) {
|
||||
logError('($type) error loading stories from web $e');
|
||||
|
||||
switch (e.runtimeType) {
|
||||
case RateLimitedException:
|
||||
case RateLimitedWithFallbackException:
|
||||
case PossibleParsingException:
|
||||
add(event.copyWith(useApi: true));
|
||||
}
|
||||
}).listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).asFuture<void>();
|
||||
add(StoryLoadingCompleted(type: type));
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,62 +204,99 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
}
|
||||
|
||||
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
|
||||
if (state.statusByType[event.type] == Status.inProgress) return;
|
||||
Future<void> onLoadMore(
|
||||
StoriesLoadMore event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
final StoryType type = event.type;
|
||||
|
||||
if (state.statusByType[type] == Status.inProgress) return;
|
||||
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
type: type,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
final int currentPage = state.currentPageByType[event.type]!;
|
||||
final int len = state.storyIdsByType[event.type]!.length;
|
||||
final int currentPage = state.currentPageByType[type]! + 1;
|
||||
|
||||
emit(
|
||||
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1),
|
||||
state.copyWithCurrentPageUpdated(type: type, to: currentPage),
|
||||
);
|
||||
final int currentPageSize = state.currentPageSize;
|
||||
final int lower = currentPageSize * (currentPage + 1);
|
||||
int upper = currentPageSize + lower;
|
||||
|
||||
if (len > lower) {
|
||||
if (len < upper) {
|
||||
upper = len;
|
||||
if (state.isOfflineReading) {
|
||||
final List<int>? ids = state.storyIdsByType[type];
|
||||
if (ids == null) {
|
||||
logError('ids should not be null.');
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: type,
|
||||
to: Status.failure,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isOfflineReading) {
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen(
|
||||
(Story story) => add(StoryLoaded(story: story, type: event.type)),
|
||||
)
|
||||
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
|
||||
} else {
|
||||
_hackerNewsRepository
|
||||
.fetchStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen(
|
||||
(Story story) => add(StoryLoaded(story: story, type: event.type)),
|
||||
)
|
||||
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
|
||||
}
|
||||
} else {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: Status.success,
|
||||
),
|
||||
final int length = ids.length;
|
||||
final int lower = min(length, _pageSize * (currentPage - 1));
|
||||
final int upper = min(length, lower + _pageSize);
|
||||
final List<int> idsForCurrentPage = ids.sublist(
|
||||
lower,
|
||||
upper,
|
||||
);
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(ids: idsForCurrentPage)
|
||||
.listen((Story story) => add(StoryLoaded(story: story, type: type)))
|
||||
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
||||
} else if (event.useApi || state.dataSource == HackerNewsDataSource.api) {
|
||||
late final int length;
|
||||
List<int>? ids = state.storyIdsByType[type];
|
||||
|
||||
if (ids == null || ids.isEmpty) {
|
||||
ids = await _hackerNewsRepository.fetchStoryIds(type: type);
|
||||
length = ids.length;
|
||||
emit(state.copyWithStoryIdsUpdated(type: type, to: ids));
|
||||
} else {
|
||||
length = ids.length;
|
||||
}
|
||||
|
||||
final int lower = min(length, _pageSize * (currentPage - 1));
|
||||
final int upper = min(length, lower + _pageSize);
|
||||
final List<int> idsForCurrentPage = ids.sublist(
|
||||
lower,
|
||||
upper,
|
||||
);
|
||||
_hackerNewsRepository
|
||||
.fetchStoriesStream(ids: idsForCurrentPage)
|
||||
.listen(
|
||||
(Story story) => add(StoryLoaded(story: story, type: type)),
|
||||
)
|
||||
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
||||
} else {
|
||||
_hackerNewsWebRepository
|
||||
.fetchStoriesStream(type, page: currentPage)
|
||||
.handleError((dynamic e) {
|
||||
logError('error loading more stories $e');
|
||||
|
||||
switch (e.runtimeType) {
|
||||
case RateLimitedException:
|
||||
case RateLimitedWithFallbackException:
|
||||
case PossibleParsingException:
|
||||
|
||||
/// Fall back to use API instead.
|
||||
add(event.copyWith(useApi: true));
|
||||
emit(
|
||||
state.copyWithCurrentPageUpdated(
|
||||
type: type,
|
||||
to: currentPage - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.listen(
|
||||
(Story story) => add(StoryLoaded(story: story, type: type)),
|
||||
)
|
||||
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,8 +312,11 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
final Story story = event.story;
|
||||
if (state.storiesByType[event.type]?.contains(story) ?? false) {
|
||||
_logger.d('story already exists.');
|
||||
if (state.storiesByType[event.type]
|
||||
?.where((Story s) => s.id == story.id)
|
||||
.isNotEmpty ??
|
||||
false) {
|
||||
logDebug('story ${story.id} for ${event.type} already exists.');
|
||||
return;
|
||||
}
|
||||
final bool hasRead = await _preferenceRepository.hasRead(story.id);
|
||||
@ -279,12 +351,16 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
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 =
|
||||
@ -293,6 +369,14 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
prioritizedIds.addAll(ids);
|
||||
}
|
||||
|
||||
prioritizedIds = prioritizedIds.toSet().toList().sublist(
|
||||
0,
|
||||
min(
|
||||
state.maxOfflineStoriesCount?.count ?? prioritizedIds.length,
|
||||
prioritizedIds.length,
|
||||
),
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
storiesDownloaded: 0,
|
||||
@ -348,20 +432,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
<StreamSubscription<Comment>>[];
|
||||
for (final int id in ids) {
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
|
||||
_logger.d('aborting downloading');
|
||||
logDebug('aborting downloading');
|
||||
|
||||
for (final StreamSubscription<Comment> stream in downloadStreams) {
|
||||
await stream.cancel();
|
||||
}
|
||||
|
||||
_logger.d('deleting downloaded contents');
|
||||
logDebug('deleting downloaded contents');
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.d('fetching story $id');
|
||||
logDebug('fetching story $id');
|
||||
final Story? story = await _hackerNewsRepository.fetchStory(id: id);
|
||||
|
||||
if (story == null) {
|
||||
@ -381,7 +465,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.cacheStory(story: story);
|
||||
|
||||
if (story.url.isNotEmpty && includingWebPage) {
|
||||
_logger.i('downloading ${story.url}');
|
||||
logInfo('downloading ${story.url}');
|
||||
await _offlineRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
@ -398,18 +482,18 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
.listen(
|
||||
(Comment comment) {
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
|
||||
_logger.d('aborting downloading from comments stream');
|
||||
logDebug('aborting downloading from comments stream');
|
||||
downloadStream?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.d('fetched comment ${comment.id}');
|
||||
logDebug('fetched comment ${comment.id}');
|
||||
unawaited(
|
||||
_offlineRepository.cacheComment(comment: comment),
|
||||
);
|
||||
},
|
||||
)..onDone(() {
|
||||
_logger.d(
|
||||
logDebug(
|
||||
'''finished downloading story ${story.id} with ${story.descendants} comments''',
|
||||
);
|
||||
add(StoryDownloaded(skipped: false));
|
||||
@ -452,13 +536,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPageSizeChanged(
|
||||
StoriesPageSizeChanged event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
Future<void> onExitOfflineMode(
|
||||
StoriesExitOfflineMode event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -475,6 +552,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
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,
|
||||
@ -516,19 +607,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
bool hasRead(Story story) => state.readStoriesIds.contains(story.id);
|
||||
|
||||
int getPageSize({required bool isComplexTile}) {
|
||||
int pageSize = isComplexTile ? _smallPageSize : _largePageSize;
|
||||
|
||||
if (deviceScreenType != DeviceScreenType.mobile) {
|
||||
pageSize = isComplexTile ? _tabletSmallPageSize : _tabletLargePageSize;
|
||||
}
|
||||
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _preferenceSubscription?.cancel();
|
||||
await super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[StoriesBloc]';
|
||||
}
|
||||
|
@ -6,19 +6,35 @@ abstract class StoriesEvent extends Equatable {
|
||||
}
|
||||
|
||||
class LoadStories extends StoriesEvent {
|
||||
LoadStories({required this.type, this.isRefreshing = false});
|
||||
LoadStories({
|
||||
required this.type,
|
||||
this.isRefreshing = false,
|
||||
this.useApi = false,
|
||||
});
|
||||
|
||||
final StoryType type;
|
||||
final bool isRefreshing;
|
||||
final bool useApi;
|
||||
|
||||
LoadStories copyWith({required bool useApi}) {
|
||||
return LoadStories(type: type, isRefreshing: isRefreshing, useApi: useApi);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
type,
|
||||
isRefreshing,
|
||||
useApi,
|
||||
];
|
||||
}
|
||||
|
||||
class StoriesInitialize extends StoriesEvent {
|
||||
StoriesInitialize({
|
||||
this.startup = false,
|
||||
});
|
||||
|
||||
final bool startup;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
@ -33,12 +49,24 @@ class StoriesRefresh extends StoriesEvent {
|
||||
}
|
||||
|
||||
class StoriesLoadMore extends StoriesEvent {
|
||||
StoriesLoadMore({required this.type});
|
||||
StoriesLoadMore({
|
||||
required this.type,
|
||||
this.useApi = false,
|
||||
});
|
||||
|
||||
final StoryType type;
|
||||
|
||||
final bool useApi;
|
||||
|
||||
StoriesLoadMore copyWith({required bool useApi}) {
|
||||
return StoriesLoadMore(type: type, useApi: useApi);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[type];
|
||||
List<Object?> get props => <Object?>[
|
||||
type,
|
||||
useApi,
|
||||
];
|
||||
}
|
||||
|
||||
class StoriesDownload extends StoriesEvent {
|
||||
@ -76,13 +104,20 @@ class StoriesEnterOfflineMode extends StoriesEvent {
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class StoriesPageSizeChanged extends StoriesEvent {
|
||||
StoriesPageSizeChanged({required this.pageSize});
|
||||
class UpdateMaxOfflineStoriesCount extends StoriesEvent {
|
||||
UpdateMaxOfflineStoriesCount({required this.count});
|
||||
|
||||
final int pageSize;
|
||||
final MaxOfflineStoriesCount count;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[pageSize];
|
||||
List<Object?> get props => <Object?>[count];
|
||||
}
|
||||
|
||||
class ClearMaxOfflineStoriesCount extends StoriesEvent {
|
||||
ClearMaxOfflineStoriesCount();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class StoryLoaded extends StoriesEvent {
|
||||
|
@ -17,9 +17,10 @@ class StoriesState extends Equatable {
|
||||
required this.readStoriesIds,
|
||||
required this.isOfflineReading,
|
||||
required this.downloadStatus,
|
||||
required this.currentPageSize,
|
||||
required this.storiesDownloaded,
|
||||
required this.storiesToBeDownloaded,
|
||||
required this.maxOfflineStoriesCount,
|
||||
required this.dataSource,
|
||||
});
|
||||
|
||||
const StoriesState.init({
|
||||
@ -53,10 +54,11 @@ class StoriesState extends Equatable {
|
||||
},
|
||||
}) : isOfflineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.idle,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{},
|
||||
storiesDownloaded = 0,
|
||||
storiesToBeDownloaded = 0;
|
||||
storiesToBeDownloaded = 0,
|
||||
maxOfflineStoriesCount = null,
|
||||
dataSource = null;
|
||||
|
||||
final Map<StoryType, List<Story>> storiesByType;
|
||||
final Map<StoryType, List<int>> storyIdsByType;
|
||||
@ -65,9 +67,10 @@ class StoriesState extends Equatable {
|
||||
final Set<int> readStoriesIds;
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
final bool isOfflineReading;
|
||||
final int currentPageSize;
|
||||
final int storiesDownloaded;
|
||||
final int storiesToBeDownloaded;
|
||||
final MaxOfflineStoriesCount? maxOfflineStoriesCount;
|
||||
final HackerNewsDataSource? dataSource;
|
||||
|
||||
StoriesState copyWith({
|
||||
Map<StoryType, List<Story>>? storiesByType,
|
||||
@ -77,9 +80,10 @@ class StoriesState extends Equatable {
|
||||
Set<int>? readStoriesIds,
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
bool? isOfflineReading,
|
||||
int? currentPageSize,
|
||||
int? storiesDownloaded,
|
||||
int? storiesToBeDownloaded,
|
||||
MaxOfflineStoriesCount? maxOfflineStoriesCount,
|
||||
HackerNewsDataSource? dataSource,
|
||||
}) {
|
||||
return StoriesState(
|
||||
storiesByType: storiesByType ?? this.storiesByType,
|
||||
@ -89,10 +93,28 @@ class StoriesState extends Equatable {
|
||||
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
|
||||
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||
downloadStatus: downloadStatus ?? this.downloadStatus,
|
||||
currentPageSize: currentPageSize ?? this.currentPageSize,
|
||||
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -179,8 +201,9 @@ class StoriesState extends Equatable {
|
||||
readStoriesIds,
|
||||
isOfflineReading,
|
||||
downloadStatus,
|
||||
currentPageSize,
|
||||
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 =
|
||||
@ -88,6 +88,7 @@ abstract class AppDurations {
|
||||
static const Duration ms600 = Duration(milliseconds: 600);
|
||||
static const Duration oneSecond = Duration(seconds: 1);
|
||||
static const Duration twoSeconds = Duration(seconds: 2);
|
||||
static const Duration fiveSeconds = Duration(seconds: 5);
|
||||
static const Duration tenSeconds = Duration(seconds: 10);
|
||||
static const Duration sec30 = Duration(seconds: 30);
|
||||
static const Duration oneMinute = Duration(minutes: 1);
|
||||
|
@ -5,13 +5,13 @@ class CustomLogFilter extends LogFilter {
|
||||
Level? get level => Level.trace;
|
||||
|
||||
/// The minimal level allowed in production.
|
||||
static const Level _minimalLevel = Level.info;
|
||||
static const Level minimalLevel = Level.info;
|
||||
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
bool shouldLog = false;
|
||||
|
||||
if (event.level.index >= _minimalLevel.index) {
|
||||
if (event.level.index >= minimalLevel.index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -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,52 +29,74 @@ 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(),
|
||||
),
|
||||
GoRoute(
|
||||
path: WebViewScreen.routeName,
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? link = state.extra as String?;
|
||||
if (link == null) {
|
||||
throw GoError("link can't be null");
|
||||
}
|
||||
return WebViewScreen(
|
||||
url: link,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: SubmitScreen.routeName,
|
||||
builder: (_, __) => BlocProvider<SubmitCubit>(
|
||||
create: (_) => SubmitCubit(),
|
||||
child: const SubmitScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: QrCodeScannerScreen.routeName,
|
||||
builder: (_, __) => const QrCodeScannerScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: QrCodeViewScreen.routeName,
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? data = state.extra as String?;
|
||||
if (data == null) {
|
||||
throw GoError("data can't be null");
|
||||
}
|
||||
return QrCodeViewScreen(
|
||||
data: data,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${ItemScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
|
||||
if (args == null) {
|
||||
throw GoError("args can't be null");
|
||||
}
|
||||
return ItemScreen.phone(args);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${SubmitScreen.routeName}',
|
||||
builder: (_, __) => BlocProvider<SubmitCubit>(
|
||||
create: (_) => SubmitCubit(),
|
||||
child: const SubmitScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${QrCodeScannerScreen.routeName}',
|
||||
builder: (_, __) => const QrCodeScannerScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${QrCodeViewScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? data = state.extra as String?;
|
||||
if (data == null) {
|
||||
throw GoError("data can't be null");
|
||||
}
|
||||
return QrCodeViewScreen(
|
||||
data: data,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${WebViewScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? link = state.extra as String?;
|
||||
if (link == null) {
|
||||
throw GoError("link can't be null");
|
||||
}
|
||||
return WebViewScreen(
|
||||
url: link,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
47
lib/config/paths.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
abstract class Paths {
|
||||
static const LogPaths log = LogPaths._();
|
||||
static const HomePaths home = HomePaths._();
|
||||
static const ItemPaths item = ItemPaths._();
|
||||
static const QrCodePaths qrCode = QrCodePaths._();
|
||||
static const WebViewPaths webView = WebViewPaths._();
|
||||
}
|
||||
|
||||
class HomePaths with RootPaths {
|
||||
const HomePaths._();
|
||||
|
||||
String get landing => rootPath('');
|
||||
}
|
||||
|
||||
class ItemPaths with RootPaths {
|
||||
const ItemPaths._();
|
||||
|
||||
String get landing => rootPath(ItemScreen.routeName);
|
||||
|
||||
String get submit => rootPath(SubmitScreen.routeName);
|
||||
}
|
||||
|
||||
class LogPaths with RootPaths {
|
||||
const LogPaths._();
|
||||
|
||||
String get landing => rootPath(LogScreen.routeName);
|
||||
}
|
||||
|
||||
class QrCodePaths with RootPaths {
|
||||
const QrCodePaths._();
|
||||
|
||||
String get scanner => rootPath(QrCodeScannerScreen.routeName);
|
||||
|
||||
String get viewer => rootPath(QrCodeViewScreen.routeName);
|
||||
}
|
||||
|
||||
class WebViewPaths with RootPaths {
|
||||
const WebViewPaths._();
|
||||
|
||||
String get landing => rootPath(WebViewScreen.routeName);
|
||||
}
|
||||
|
||||
mixin RootPaths {
|
||||
String rootPath(String path) => '/$path';
|
||||
}
|
@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/config/paths.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -17,13 +18,12 @@ import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:linkify/linkify.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
part 'comments_state.dart';
|
||||
|
||||
class CommentsCubit extends Cubit<CommentsState> {
|
||||
class CommentsCubit extends Cubit<CommentsState> with Loggable {
|
||||
CommentsCubit({
|
||||
required FilterCubit filterCubit,
|
||||
required PreferenceCubit preferenceCubit,
|
||||
@ -37,7 +37,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
SembastRepository? sembastRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
HackerNewsWebRepository? hackerNewsWebRepository,
|
||||
Logger? logger,
|
||||
}) : _filterCubit = filterCubit,
|
||||
_preferenceCubit = preferenceCubit,
|
||||
_collapseCache = collapseCache,
|
||||
@ -50,7 +49,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_hackerNewsWebRepository =
|
||||
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(
|
||||
CommentsState.init(
|
||||
isOfflineReading: isOfflineReading,
|
||||
@ -68,7 +66,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
final SembastRepository _sembastRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final HackerNewsWebRepository _hackerNewsWebRepository;
|
||||
final Logger _logger;
|
||||
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
final ItemPositionsListener itemPositionsListener =
|
||||
@ -182,13 +179,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
case CommentsOrder.natural:
|
||||
final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
|
||||
if (fetchFromWeb && shouldFetchFromWeb) {
|
||||
_logger.d('fetching from web.');
|
||||
logDebug('fetching comments of ${item.id} from web.');
|
||||
commentStream = _hackerNewsWebRepository
|
||||
.fetchCommentsStream(state.item)
|
||||
.handleError((dynamic e) {
|
||||
_streamSubscription?.cancel();
|
||||
|
||||
_logger.e(e);
|
||||
logError(e);
|
||||
|
||||
switch (e.runtimeType) {
|
||||
case RateLimitedException:
|
||||
@ -205,7 +202,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_logger.d('fetching from API.');
|
||||
logDebug('fetching comments of ${item.id} from API.');
|
||||
commentStream =
|
||||
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
@ -280,11 +277,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
case CommentsOrder.natural:
|
||||
final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
|
||||
if (fetchFromWeb && shouldFetchFromWeb) {
|
||||
_logger.d('fetching from web.');
|
||||
logDebug(
|
||||
'fetching comments of ${item.id} from web.',
|
||||
);
|
||||
commentStream = _hackerNewsWebRepository
|
||||
.fetchCommentsStream(state.item)
|
||||
.handleError((dynamic e) {
|
||||
_logger.e(e);
|
||||
logError(e);
|
||||
|
||||
switch (e.runtimeType) {
|
||||
case RateLimitedException:
|
||||
@ -301,7 +300,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_logger.d('fetching from API.');
|
||||
logDebug('fetching comments of ${item.id} from API.');
|
||||
commentStream = _hackerNewsRepository
|
||||
.fetchAllCommentsRecursivelyStream(ids: kids);
|
||||
}
|
||||
@ -385,8 +384,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
_streamSubscriptions[comment.id]?.cancel();
|
||||
_streamSubscriptions.remove(comment.id);
|
||||
})
|
||||
..onError((dynamic error) {
|
||||
_logger.e(error);
|
||||
..onError((dynamic e) {
|
||||
logError(e);
|
||||
_streamSubscriptions[comment.id]?.cancel();
|
||||
_streamSubscriptions.remove(comment.id);
|
||||
});
|
||||
@ -412,7 +411,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
return;
|
||||
} else {
|
||||
await router.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
Paths.item.landing,
|
||||
extra: ItemScreenArgs(item: parent),
|
||||
);
|
||||
|
||||
@ -435,7 +434,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
return;
|
||||
} else {
|
||||
await router.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
Paths.item.landing,
|
||||
extra: ItemScreenArgs(item: parent),
|
||||
);
|
||||
|
||||
@ -500,7 +499,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
? state.comments.elementAt(e.index - 1)
|
||||
: null,
|
||||
)
|
||||
.whereNotNull()
|
||||
.nonNulls
|
||||
.toList();
|
||||
|
||||
if (onScreenComments.isEmpty && state.comments.isNotEmpty) {
|
||||
@ -521,7 +520,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
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);
|
||||
@ -558,7 +557,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
? state.comments.elementAt(e.index - 1)
|
||||
: null,
|
||||
)
|
||||
.whereNotNull()
|
||||
.nonNulls
|
||||
.toList();
|
||||
|
||||
/// The index of first comment visible on screen.
|
||||
@ -732,4 +731,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
await super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[CommentsCubit]';
|
||||
}
|
||||
|
@ -6,20 +6,20 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'fav_state.dart';
|
||||
|
||||
class FavCubit extends Cubit<FavState> {
|
||||
class FavCubit extends Cubit<FavState> with Loggable {
|
||||
FavCubit({
|
||||
required AuthBloc authBloc,
|
||||
AuthRepository? authRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
HackerNewsWebRepository? hackerNewsWebRepository,
|
||||
Logger? logger,
|
||||
SembastRepository? sembastRepository,
|
||||
}) : _authBloc = authBloc,
|
||||
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||
_preferenceRepository =
|
||||
@ -28,7 +28,8 @@ class FavCubit extends Cubit<FavState> {
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_hackerNewsWebRepository =
|
||||
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(FavState.init()) {
|
||||
init();
|
||||
}
|
||||
@ -38,9 +39,9 @@ class FavCubit extends Cubit<FavState> {
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final HackerNewsWebRepository _hackerNewsWebRepository;
|
||||
final Logger _logger;
|
||||
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
|
||||
@ -58,6 +59,8 @@ class FavCubit extends Cubit<FavState> {
|
||||
_hackerNewsRepository
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
getFromCache: (int id) =>
|
||||
_sembastRepository.getCachedItem(id: id),
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
@ -100,7 +103,10 @@ class FavCubit extends Cubit<FavState> {
|
||||
void removeFav(int id) {
|
||||
_preferenceRepository
|
||||
..removeFav(username: username, id: id)
|
||||
..removeFav(username: '', id: id);
|
||||
..removeFav(
|
||||
username: '',
|
||||
id: id,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -184,7 +190,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
final Iterable<int> ids = await _hackerNewsWebRepository.fetchFavorites(
|
||||
of: _authBloc.state.username,
|
||||
);
|
||||
_logger.d('fetched ${ids.length} favorite items from HN.');
|
||||
logDebug('fetched ${ids.length} favorite items from HN.');
|
||||
final List<int> combinedIds = <int>[...ids, ...state.favIds];
|
||||
final LinkedHashSet<int> mergedIds =
|
||||
LinkedHashSet<int>.from(combinedIds);
|
||||
@ -203,6 +209,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
}
|
||||
|
||||
void _onItemLoaded(Item item) {
|
||||
_sembastRepository.cacheItem(item);
|
||||
emit(
|
||||
state.copyWith(
|
||||
favItems: List<Item>.from(state.favItems)..add(item),
|
||||
@ -210,11 +217,17 @@ class FavCubit extends Cubit<FavState> {
|
||||
);
|
||||
}
|
||||
|
||||
void switchTab() =>
|
||||
emit(state.copyWith(isDisplayingStories: !state.isDisplayingStories));
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_usernameSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[FavCubit]';
|
||||
}
|
||||
|
||||
extension on FavCubit {
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -7,20 +7,19 @@ import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'notification_state.dart';
|
||||
|
||||
class NotificationCubit extends Cubit<NotificationState> {
|
||||
class NotificationCubit extends Cubit<NotificationState> with Loggable {
|
||||
NotificationCubit({
|
||||
required AuthBloc authBloc,
|
||||
required PreferenceCubit preferenceCubit,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
Logger? logger,
|
||||
}) : _authBloc = authBloc,
|
||||
_preferenceCubit = preferenceCubit,
|
||||
_hackerNewsRepository =
|
||||
@ -29,7 +28,6 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(NotificationState.init()) {
|
||||
_authBloc.stream
|
||||
.map((AuthState event) => event.username)
|
||||
@ -61,7 +59,6 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
final Logger _logger;
|
||||
Timer? _timer;
|
||||
|
||||
static const Duration _refreshInterval = Duration(minutes: 5);
|
||||
@ -78,7 +75,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
});
|
||||
|
||||
await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) {
|
||||
_logger.i('NotificationCubit: ${unreadIds.length} unread items.');
|
||||
logInfo('${unreadIds.length} unread items.');
|
||||
emit(state.copyWith(unreadCommentsIds: unreadIds));
|
||||
});
|
||||
|
||||
@ -104,31 +101,17 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
}
|
||||
|
||||
void markAsRead(int id) {
|
||||
Future.doWhile(() {
|
||||
if (state.status != Status.inProgress) {
|
||||
if (state.unreadCommentsIds.contains(id)) {
|
||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||
..remove(id);
|
||||
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
||||
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
if (state.unreadCommentsIds.contains(id)) {
|
||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||
..remove(id);
|
||||
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
||||
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
||||
}
|
||||
}
|
||||
|
||||
void markAllAsRead() {
|
||||
Future.doWhile(() {
|
||||
if (state.status != Status.inProgress) {
|
||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
@ -274,4 +257,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
then?.call(res);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[NotificationCubit]';
|
||||
}
|
||||
|
@ -6,23 +6,19 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'preference_state.dart';
|
||||
|
||||
class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
class PreferenceCubit extends Cubit<PreferenceState> with Loggable {
|
||||
PreferenceCubit({
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(PreferenceState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
|
||||
void init() {
|
||||
for (final BooleanPreference p
|
||||
@ -73,7 +69,7 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
}
|
||||
|
||||
void update<T>(Preference<T> preference) {
|
||||
_logger.i('updating $preference to ${preference.val}');
|
||||
logInfo('updating $preference to ${preference.val}');
|
||||
|
||||
emit(state.copyWithPreference(preference));
|
||||
|
||||
@ -97,4 +93,7 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[PreferenceCubit]';
|
||||
}
|
||||
|
@ -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>();
|
||||
|
||||
@ -89,6 +89,12 @@ class PreferenceState extends Equatable {
|
||||
) as MaterialColor;
|
||||
}
|
||||
|
||||
HackerNewsDataSource get dataSource {
|
||||
return HackerNewsDataSource.values.elementAt(
|
||||
preferences.singleWhereType<HackerNewsDataSourcePreference>().val,
|
||||
);
|
||||
}
|
||||
|
||||
List<StoryType> get tabs {
|
||||
final String result =
|
||||
preferences.singleWhereType<TabOrderPreference>().val.toString();
|
||||
|
@ -1,14 +1,16 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/repositories/remote_config_repository.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
part 'remote_config_state.dart';
|
||||
|
||||
class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> {
|
||||
RemoteConfigCubit({RemoteConfigRepository? remoteConfigRepository})
|
||||
: _remoteConfigRepository =
|
||||
class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> with Loggable {
|
||||
RemoteConfigCubit({
|
||||
RemoteConfigRepository? remoteConfigRepository,
|
||||
}) : _remoteConfigRepository =
|
||||
remoteConfigRepository ?? locator.get<RemoteConfigRepository>(),
|
||||
super(RemoteConfigState.init()) {
|
||||
init();
|
||||
@ -21,7 +23,10 @@ class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> {
|
||||
.fetchRemoteConfig()
|
||||
.then((Map<String, dynamic> data) {
|
||||
if (data.isNotEmpty) {
|
||||
logInfo('remote config fetched: $data');
|
||||
emit(state.copyWith(data: data));
|
||||
} else {
|
||||
logInfo('remote config fetched is empty.');
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -35,4 +40,7 @@ class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> {
|
||||
Map<String, dynamic>? toJson(RemoteConfigState state) {
|
||||
return state.data;
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => 'RemoteConfigCubit';
|
||||
}
|
||||
|
@ -10,6 +10,48 @@ final class RemoteConfigState extends Equatable {
|
||||
@protected
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
String get storySelector => getString(
|
||||
key: 'storySelector',
|
||||
fallback: '''#hnmain > tbody > tr > td > table > tbody > .athing''',
|
||||
);
|
||||
|
||||
String get subtextSelector => getString(
|
||||
key: 'subtextSelector',
|
||||
fallback:
|
||||
'''#hnmain > tbody > tr > td > table > tbody > tr > .subtext''',
|
||||
);
|
||||
|
||||
String get titlelineSelector => getString(
|
||||
key: 'titlelineSelector',
|
||||
fallback: '''.title > .titleline > a''',
|
||||
);
|
||||
|
||||
String get pointSelector => getString(
|
||||
key: 'pointSelector',
|
||||
fallback: '''.subline > .score''',
|
||||
);
|
||||
|
||||
String get userSelector => getString(
|
||||
key: 'userSelector',
|
||||
fallback: '''.subline > .hnuser''',
|
||||
);
|
||||
|
||||
String get ageSelector => getString(
|
||||
key: 'ageSelector',
|
||||
fallback: '''.subline > .age''',
|
||||
);
|
||||
|
||||
String get cmtCountSelector => getString(
|
||||
key: 'cmtCountSelector',
|
||||
fallback: '''.subline > a''',
|
||||
);
|
||||
|
||||
String get moreLinkSelector => getString(
|
||||
key: 'moreLinkSelector',
|
||||
fallback:
|
||||
''''#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a''',
|
||||
);
|
||||
|
||||
String get athingComtrSelector => getString(
|
||||
key: 'athingComtrSelector',
|
||||
fallback:
|
||||
|
@ -1,25 +1,23 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
part 'split_view_state.dart';
|
||||
|
||||
class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
class SplitViewCubit extends HydratedCubit<SplitViewState> with Loggable {
|
||||
SplitViewCubit({
|
||||
CommentCache? commentCache,
|
||||
Logger? logger,
|
||||
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const SplitViewState.init());
|
||||
|
||||
final Logger _logger;
|
||||
final CommentCache _commentCache;
|
||||
|
||||
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||
_logger.i('resetting comments in CommentCache');
|
||||
logInfo('resetting comments in CommentCache');
|
||||
_commentCache.resetComments();
|
||||
emit(state.copyWith(itemScreenArgs: args));
|
||||
}
|
||||
@ -28,5 +26,36 @@ class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
|
||||
void disableSplitView() => emit(state.copyWith(enabled: false));
|
||||
|
||||
void zoom() => emit(state.copyWith(expanded: !state.expanded));
|
||||
void zoom() => emit(
|
||||
state.copyWith(
|
||||
expanded: !state.expanded,
|
||||
resizingAnimationDuration: AppDurations.ms300,
|
||||
),
|
||||
);
|
||||
|
||||
void updateSubmissionPanelWidth(double width) => emit(
|
||||
state.copyWith(
|
||||
submissionPanelWidth: width,
|
||||
resizingAnimationDuration: Duration.zero,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[SplitViewCubit]';
|
||||
|
||||
static const String _submissionPanelWidthKey = 'submissionPanelWidth';
|
||||
|
||||
@override
|
||||
SplitViewState? fromJson(Map<String, dynamic> json) {
|
||||
return state.copyWith(
|
||||
submissionPanelWidth: json[_submissionPanelWidthKey] as double?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(SplitViewState state) {
|
||||
return <String, dynamic>{
|
||||
_submissionPanelWidthKey: state.submissionPanelWidth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,36 @@ class SplitViewState extends Equatable {
|
||||
required this.itemScreenArgs,
|
||||
required this.expanded,
|
||||
required this.enabled,
|
||||
required this.resizingAnimationDuration,
|
||||
this.submissionPanelWidth,
|
||||
});
|
||||
|
||||
const SplitViewState.init()
|
||||
: enabled = false,
|
||||
expanded = false,
|
||||
submissionPanelWidth = null,
|
||||
resizingAnimationDuration = Duration.zero,
|
||||
itemScreenArgs = null;
|
||||
|
||||
final bool enabled;
|
||||
final bool expanded;
|
||||
final double? submissionPanelWidth;
|
||||
final Duration resizingAnimationDuration;
|
||||
final ItemScreenArgs? itemScreenArgs;
|
||||
|
||||
SplitViewState copyWith({
|
||||
bool? enabled,
|
||||
bool? expanded,
|
||||
double? submissionPanelWidth,
|
||||
Duration? resizingAnimationDuration,
|
||||
ItemScreenArgs? itemScreenArgs,
|
||||
}) {
|
||||
return SplitViewState(
|
||||
enabled: enabled ?? this.enabled,
|
||||
expanded: expanded ?? this.expanded,
|
||||
submissionPanelWidth: submissionPanelWidth ?? this.submissionPanelWidth,
|
||||
resizingAnimationDuration:
|
||||
resizingAnimationDuration ?? this.resizingAnimationDuration,
|
||||
itemScreenArgs: itemScreenArgs ?? this.itemScreenArgs,
|
||||
);
|
||||
}
|
||||
@ -32,6 +43,8 @@ class SplitViewState extends Equatable {
|
||||
List<Object?> get props => <Object?>[
|
||||
enabled,
|
||||
expanded,
|
||||
submissionPanelWidth,
|
||||
resizingAnimationDuration,
|
||||
itemScreenArgs,
|
||||
];
|
||||
}
|
||||
|
@ -1,40 +1,38 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'tab_state.dart';
|
||||
|
||||
class TabCubit extends Cubit<TabState> {
|
||||
class TabCubit extends Cubit<TabState> with Loggable {
|
||||
TabCubit({
|
||||
required PreferenceCubit preferenceCubit,
|
||||
Logger? logger,
|
||||
}) : _preferenceCubit = preferenceCubit,
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(TabState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final Logger _logger;
|
||||
|
||||
void init() {
|
||||
final List<StoryType> tabs = _preferenceCubit.state.tabs;
|
||||
|
||||
_logger.i('updating tabs to $tabs');
|
||||
logInfo('updating tabs to $tabs');
|
||||
|
||||
emit(state.copyWith(tabs: tabs));
|
||||
}
|
||||
|
||||
void update(int startIndex, int endIndex) {
|
||||
_logger.d('updating ${state.tabs} by moving $startIndex to $endIndex');
|
||||
logDebug(
|
||||
'updating ${state.tabs} by moving $startIndex to $endIndex',
|
||||
);
|
||||
final StoryType tab = state.tabs.elementAt(startIndex);
|
||||
final List<StoryType> updatedTabs = List<StoryType>.from(state.tabs)
|
||||
..insert(endIndex, tab)
|
||||
..removeAt(startIndex < endIndex ? startIndex : startIndex + 1);
|
||||
_logger.d(updatedTabs);
|
||||
logDebug(updatedTabs);
|
||||
emit(state.copyWith(tabs: updatedTabs));
|
||||
|
||||
// Check to make sure there's no duplicate.
|
||||
@ -44,4 +42,7 @@ class TabCubit extends Cubit<TabState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get logIdentifier => '[TabCubit]';
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ export 'date_time_extension.dart';
|
||||
export 'int_extension.dart';
|
||||
export 'item_action_mixin.dart';
|
||||
export 'list_extension.dart';
|
||||
export 'object_extension.dart';
|
||||
export 'loggable.dart';
|
||||
export 'set_extension.dart';
|
||||
export 'string_extension.dart';
|
||||
export 'widget_extension.dart';
|
||||
|
@ -3,12 +3,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/paths.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/item/models/models.dart';
|
||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
|
||||
import 'package:hacki/screens/screens.dart' show ItemScreenArgs;
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@ -39,7 +40,7 @@ mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
|
||||
if (splitViewEnabled && !forceNewScreen) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
context.push('/${ItemScreen.routeName}', extra: args);
|
||||
context.push(Paths.item.landing, extra: args);
|
||||
}
|
||||
|
||||
return Future<void>.value();
|
||||
@ -128,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -164,7 +167,7 @@ mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
|
||||
);
|
||||
},
|
||||
).then((bool? yesTapped) {
|
||||
if (yesTapped ?? false) {
|
||||
if (mounted && (yesTapped ?? false)) {
|
||||
context.read<AuthBloc>().add(AuthFlag(item: item));
|
||||
showSnackBar(content: 'Comment flagged!');
|
||||
}
|
||||
@ -202,6 +205,8 @@ mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
|
||||
);
|
||||
},
|
||||
).then((bool? yesTapped) {
|
||||
if (!mounted) return;
|
||||
|
||||
if (yesTapped ?? false) {
|
||||
if (isBlocked) {
|
||||
context.read<BlocklistCubit>().removeFromBlocklist(item.by);
|
||||
|
@ -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);
|
||||
|