Initial commit
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
42
.metadata
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: android
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# pixes
|
||||||
|
|
||||||
|
Unoffcial pixiv app
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
28
analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
13
android/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
67
android/app/build.gradle
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.github.wgh136.pixes"
|
||||||
|
compileSdk flutter.compileSdkVersion
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "com.github.wgh136.pixes"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
minSdkVersion flutter.minSdkVersion
|
||||||
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {}
|
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
44
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="pixes"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility?hl=en and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
@@ -0,0 +1,5 @@
|
|||||||
|
package com.github.wgh136.pixes
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity()
|
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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> -->
|
||||||
|
</layer-list>
|
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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> -->
|
||||||
|
</layer-list>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</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>
|
18
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</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>
|
7
android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
18
android/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
3
android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4G
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
26
android/settings.gradle
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.7.10" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
34
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
26
ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>12.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
1
ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
1
ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
616
ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
98
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<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">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<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>
|
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
13
ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import UIKit
|
||||||
|
import Flutter
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 295 B |
After Width: | Height: | Size: 406 B |
After Width: | Height: | Size: 450 B |
After Width: | Height: | Size: 282 B |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 704 B |
After Width: | Height: | Size: 406 B |
After Width: | Height: | Size: 586 B |
After Width: | Height: | Size: 862 B |
After Width: | Height: | Size: 862 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 762 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<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>
|
||||||
|
</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"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
49
ios/Runner/Info.plist
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Pixes</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>pixes</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
12
ios/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
lib/appdata.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'foundation/app.dart';
|
||||||
|
import 'network/models.dart';
|
||||||
|
|
||||||
|
class _Appdata {
|
||||||
|
Account? account;
|
||||||
|
|
||||||
|
void writeData() async {
|
||||||
|
await File("${App.dataPath}/account.json")
|
||||||
|
.writeAsString(jsonEncode(account));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> readData() async {
|
||||||
|
final file = File("${App.dataPath}/account.json");
|
||||||
|
if (file.existsSync()) {
|
||||||
|
account = Account.fromJson(jsonDecode(await file.readAsString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final appdata = _Appdata();
|
321
lib/components/animated_image.dart
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
class AnimatedImage extends StatefulWidget {
|
||||||
|
/// show animation when loading is complete.
|
||||||
|
AnimatedImage({
|
||||||
|
required ImageProvider image,
|
||||||
|
super.key,
|
||||||
|
double scale = 1.0,
|
||||||
|
this.semanticLabel,
|
||||||
|
this.excludeFromSemantics = false,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.color,
|
||||||
|
this.opacity,
|
||||||
|
this.colorBlendMode,
|
||||||
|
this.fit,
|
||||||
|
this.alignment = Alignment.center,
|
||||||
|
this.repeat = ImageRepeat.noRepeat,
|
||||||
|
this.centerSlice,
|
||||||
|
this.matchTextDirection = false,
|
||||||
|
this.gaplessPlayback = false,
|
||||||
|
this.filterQuality = FilterQuality.low,
|
||||||
|
this.isAntiAlias = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
int? cacheWidth,
|
||||||
|
int? cacheHeight,
|
||||||
|
}
|
||||||
|
): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
||||||
|
assert(cacheWidth == null || cacheWidth > 0),
|
||||||
|
assert(cacheHeight == null || cacheHeight > 0);
|
||||||
|
|
||||||
|
final ImageProvider image;
|
||||||
|
|
||||||
|
final String? semanticLabel;
|
||||||
|
|
||||||
|
final bool excludeFromSemantics;
|
||||||
|
|
||||||
|
final double? width;
|
||||||
|
|
||||||
|
final double? height;
|
||||||
|
|
||||||
|
final bool gaplessPlayback;
|
||||||
|
|
||||||
|
final bool matchTextDirection;
|
||||||
|
|
||||||
|
final Rect? centerSlice;
|
||||||
|
|
||||||
|
final ImageRepeat repeat;
|
||||||
|
|
||||||
|
final AlignmentGeometry alignment;
|
||||||
|
|
||||||
|
final BoxFit? fit;
|
||||||
|
|
||||||
|
final BlendMode? colorBlendMode;
|
||||||
|
|
||||||
|
final FilterQuality filterQuality;
|
||||||
|
|
||||||
|
final Animation<double>? opacity;
|
||||||
|
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
final bool isAntiAlias;
|
||||||
|
|
||||||
|
static void clear() => _AnimatedImageState.clear();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimatedImage> createState() => _AnimatedImageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserver {
|
||||||
|
ImageStream? _imageStream;
|
||||||
|
ImageInfo? _imageInfo;
|
||||||
|
ImageChunkEvent? _loadingProgress;
|
||||||
|
bool _isListeningToStream = false;
|
||||||
|
late bool _invertColors;
|
||||||
|
int? _frameNumber;
|
||||||
|
bool _wasSynchronouslyLoaded = false;
|
||||||
|
late DisposableBuildContext<State<AnimatedImage>> _scrollAwareContext;
|
||||||
|
Object? _lastException;
|
||||||
|
ImageStreamCompleterHandle? _completerHandle;
|
||||||
|
|
||||||
|
static final Map<int, Size> _cache = {};
|
||||||
|
|
||||||
|
static clear() => _cache.clear();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
_scrollAwareContext = DisposableBuildContext<State<AnimatedImage>>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
assert(_imageStream != null);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_stopListeningToStream();
|
||||||
|
_completerHandle?.dispose();
|
||||||
|
_scrollAwareContext.dispose();
|
||||||
|
_replaceImage(info: null);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
_updateInvertColors();
|
||||||
|
_resolveImage();
|
||||||
|
|
||||||
|
if (TickerMode.of(context)) {
|
||||||
|
_listenToStream();
|
||||||
|
} else {
|
||||||
|
_stopListeningToStream(keepStreamAlive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(AnimatedImage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.image != oldWidget.image) {
|
||||||
|
_resolveImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAccessibilityFeatures() {
|
||||||
|
super.didChangeAccessibilityFeatures();
|
||||||
|
setState(() {
|
||||||
|
_updateInvertColors();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
_resolveImage(); // in case the image cache was flushed
|
||||||
|
super.reassemble();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateInvertColors() {
|
||||||
|
_invertColors = MediaQuery.maybeInvertColorsOf(context)
|
||||||
|
?? SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resolveImage() {
|
||||||
|
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
|
||||||
|
context: _scrollAwareContext,
|
||||||
|
imageProvider: widget.image,
|
||||||
|
);
|
||||||
|
final ImageStream newStream =
|
||||||
|
provider.resolve(createLocalImageConfiguration(
|
||||||
|
context,
|
||||||
|
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
|
||||||
|
));
|
||||||
|
_updateSourceStream(newStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageStreamListener? _imageStreamListener;
|
||||||
|
ImageStreamListener _getListener({bool recreateListener = false}) {
|
||||||
|
if(_imageStreamListener == null || recreateListener) {
|
||||||
|
_lastException = null;
|
||||||
|
_imageStreamListener = ImageStreamListener(
|
||||||
|
_handleImageFrame,
|
||||||
|
onChunk: _handleImageChunk,
|
||||||
|
onError: (Object error, StackTrace? stackTrace) {
|
||||||
|
setState(() {
|
||||||
|
_lastException = error;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _imageStreamListener!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
|
||||||
|
setState(() {
|
||||||
|
_replaceImage(info: imageInfo);
|
||||||
|
_loadingProgress = null;
|
||||||
|
_lastException = null;
|
||||||
|
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
|
||||||
|
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleImageChunk(ImageChunkEvent event) {
|
||||||
|
setState(() {
|
||||||
|
_loadingProgress = event;
|
||||||
|
_lastException = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _replaceImage({required ImageInfo? info}) {
|
||||||
|
final ImageInfo? oldImageInfo = _imageInfo;
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
|
||||||
|
_imageInfo = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates _imageStream to newStream, and moves the stream listener
|
||||||
|
// registration from the old stream to the new stream (if a listener was
|
||||||
|
// registered).
|
||||||
|
void _updateSourceStream(ImageStream newStream) {
|
||||||
|
if (_imageStream?.key == newStream.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isListeningToStream) {
|
||||||
|
_imageStream!.removeListener(_getListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.gaplessPlayback) {
|
||||||
|
setState(() { _replaceImage(info: null); });
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_loadingProgress = null;
|
||||||
|
_frameNumber = null;
|
||||||
|
_wasSynchronouslyLoaded = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
_imageStream = newStream;
|
||||||
|
if (_isListeningToStream) {
|
||||||
|
_imageStream!.addListener(_getListener());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToStream() {
|
||||||
|
if (_isListeningToStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_imageStream!.addListener(_getListener());
|
||||||
|
_completerHandle?.dispose();
|
||||||
|
_completerHandle = null;
|
||||||
|
|
||||||
|
_isListeningToStream = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops listening to the image stream, if this state object has attached a
|
||||||
|
/// listener.
|
||||||
|
///
|
||||||
|
/// If the listener from this state is the last listener on the stream, the
|
||||||
|
/// stream will be disposed. To keep the stream alive, set `keepStreamAlive`
|
||||||
|
/// to true, which create [ImageStreamCompleterHandle] to keep the completer
|
||||||
|
/// alive and is compatible with the [TickerMode] being off.
|
||||||
|
void _stopListeningToStream({bool keepStreamAlive = false}) {
|
||||||
|
if (!_isListeningToStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
|
||||||
|
_completerHandle = _imageStream!.completer!.keepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
_imageStream!.removeListener(_getListener());
|
||||||
|
_isListeningToStream = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget result;
|
||||||
|
|
||||||
|
if(_imageInfo != null){
|
||||||
|
// build image
|
||||||
|
result = RawImage(
|
||||||
|
// Do not clone the image, because RawImage is a stateless wrapper.
|
||||||
|
// The image will be disposed by this state object when it is not needed
|
||||||
|
// anymore, such as when it is unmounted or when the image stream pushes
|
||||||
|
// a new image.
|
||||||
|
image: _imageInfo?.image,
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
debugImageLabel: _imageInfo?.debugLabel,
|
||||||
|
scale: _imageInfo?.scale ?? 1.0,
|
||||||
|
color: widget.color,
|
||||||
|
opacity: widget.opacity,
|
||||||
|
colorBlendMode: widget.colorBlendMode,
|
||||||
|
alignment: widget.alignment,
|
||||||
|
repeat: widget.repeat,
|
||||||
|
centerSlice: widget.centerSlice,
|
||||||
|
matchTextDirection: widget.matchTextDirection,
|
||||||
|
invertColors: _invertColors,
|
||||||
|
isAntiAlias: widget.isAntiAlias,
|
||||||
|
filterQuality: widget.filterQuality,
|
||||||
|
fit: widget.fit,
|
||||||
|
);
|
||||||
|
} else if (_lastException != null) {
|
||||||
|
result = const Center(
|
||||||
|
child: Icon(FluentIcons.error),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!widget.excludeFromSemantics) {
|
||||||
|
result = Semantics(
|
||||||
|
container: widget.semanticLabel != null,
|
||||||
|
image: true,
|
||||||
|
label: widget.semanticLabel ?? '',
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
result = const Center();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
reverseDuration: const Duration(milliseconds: 200),
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||||
|
super.debugFillProperties(description);
|
||||||
|
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
|
||||||
|
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
|
||||||
|
description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));
|
||||||
|
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
|
||||||
|
description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
|
||||||
|
}
|
||||||
|
}
|
38
lib/components/color_scheme.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class ColorScheme extends InheritedWidget{
|
||||||
|
final Brightness brightness;
|
||||||
|
|
||||||
|
const ColorScheme({super.key, required this.brightness, required super.child});
|
||||||
|
|
||||||
|
static ColorScheme of(BuildContext context){
|
||||||
|
return context.dependOnInheritedWidgetOfExactType<ColorScheme>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _light => brightness == Brightness.light;
|
||||||
|
|
||||||
|
Color get primary => _light ? const Color(0xff00538a) : const Color(0xff9ccaff);
|
||||||
|
|
||||||
|
Color get primaryContainer => _light ? const Color(0xff5fbdff) : const Color(0xff0079c5);
|
||||||
|
|
||||||
|
Color get secondary => _light ? const Color(0xff426182) : const Color(0xffaac9ef);
|
||||||
|
|
||||||
|
Color get secondaryContainer => _light ? const Color(0xffc1dcff) : const Color(0xff1f3f5f);
|
||||||
|
|
||||||
|
Color get tertiary => _light ? const Color(0xff743192) : const Color(0xffebb2ff);
|
||||||
|
|
||||||
|
Color get tertiaryContainer => _light ? const Color(0xffcf9ae8) : const Color(0xff9c58ba);
|
||||||
|
|
||||||
|
Color get outline => _light ? const Color(0xff707883) : const Color(0xff89919d);
|
||||||
|
|
||||||
|
Color get outlineVariant => _light ? const Color(0xffbfc7d3) : const Color(0xff404752);
|
||||||
|
|
||||||
|
Color get errorColor => _light ? const Color(0xffff3131) : const Color(0xfff86a6a);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
|
||||||
|
return oldWidget is!ColorScheme || brightness != oldWidget.brightness;
|
||||||
|
}
|
||||||
|
}
|
47
lib/components/illust_widget.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/models.dart';
|
||||||
|
|
||||||
|
import '../pages/illust_page.dart';
|
||||||
|
|
||||||
|
class IllustWidget extends StatelessWidget {
|
||||||
|
const IllustWidget(this.illust, {super.key});
|
||||||
|
|
||||||
|
final Illust illust;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
|
final width = constrains.maxWidth;
|
||||||
|
final height = illust.height * width / illust.width;
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||||
|
child: Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: (){
|
||||||
|
context.to(() => IllustPage(illust));
|
||||||
|
},
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(illust.images.first.medium),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: width-16.0,
|
||||||
|
height: height-16.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
106
lib/components/loading.dart
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/network/res.dart';
|
||||||
|
|
||||||
|
abstract class LoadingState<T extends StatefulWidget, S extends Object> extends State<T>{
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
S? data;
|
||||||
|
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
Future<Res<S>> loadData();
|
||||||
|
|
||||||
|
Widget buildContent(BuildContext context, S data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if(isLoading){
|
||||||
|
loadData().then((value) {
|
||||||
|
if(value.success) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
data = value.data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
error = value.errorMessage!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return const Center(
|
||||||
|
child: ProgressRing(),
|
||||||
|
);
|
||||||
|
} else if (error != null){
|
||||||
|
return Center(
|
||||||
|
child: Text(error!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return buildContent(context, data!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object> extends State<T>{
|
||||||
|
bool _isFirstLoading = true;
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
List<S>? _data;
|
||||||
|
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
int _page = 1;
|
||||||
|
|
||||||
|
Future<Res<List<S>>> loadData(int page);
|
||||||
|
|
||||||
|
Widget buildContent(BuildContext context, final List<S> data);
|
||||||
|
|
||||||
|
bool get isLoading => _isLoading || _isFirstLoading;
|
||||||
|
|
||||||
|
void nextPage() {
|
||||||
|
if(_isLoading) return;
|
||||||
|
_isLoading = true;
|
||||||
|
loadData(_page).then((value) {
|
||||||
|
_isLoading = false;
|
||||||
|
if(value.success) {
|
||||||
|
_page++;
|
||||||
|
setState(() {
|
||||||
|
_data!.addAll(value.data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if(_isFirstLoading){
|
||||||
|
loadData(_page).then((value) {
|
||||||
|
if(value.success) {
|
||||||
|
_page++;
|
||||||
|
setState(() {
|
||||||
|
_isFirstLoading = false;
|
||||||
|
_data = value.data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_isFirstLoading = false;
|
||||||
|
_error = value.errorMessage!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return const Center(
|
||||||
|
child: ProgressRing(),
|
||||||
|
);
|
||||||
|
} else if (_error != null){
|
||||||
|
return Center(
|
||||||
|
child: Text(_error!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return buildContent(context, _data!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
lib/components/md.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef MdIcons = Icons;
|
103
lib/components/message.dart
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
|
void showToast(BuildContext context, {required String message, IconData? icon}) {
|
||||||
|
var newEntry = OverlayEntry(
|
||||||
|
builder: (context) => ToastOverlay(message: message, icon: icon));
|
||||||
|
|
||||||
|
OverlayWidget.of(context)?.addOverlay(newEntry);
|
||||||
|
|
||||||
|
Timer(const Duration(seconds: 2), () => OverlayWidget.of(context)?.remove(newEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToastOverlay extends StatelessWidget {
|
||||||
|
const ToastOverlay({required this.message, this.icon, super.key});
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
bottom: 24 + MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: PhysicalModel(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
elevation: 1,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (icon != null) Icon(icon),
|
||||||
|
if (icon != null)
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverlayWidget extends StatefulWidget {
|
||||||
|
const OverlayWidget(this.child, {super.key});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
static OverlayWidgetState? of(BuildContext context) {
|
||||||
|
return LookupBoundary.findAncestorStateOfType<OverlayWidgetState>(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OverlayWidget> createState() => OverlayWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverlayWidgetState extends State<OverlayWidget> {
|
||||||
|
var overlayKey = GlobalKey<OverlayState>();
|
||||||
|
|
||||||
|
var entries = <OverlayEntry>[];
|
||||||
|
|
||||||
|
void addOverlay(OverlayEntry entry) {
|
||||||
|
if (overlayKey.currentState != null) {
|
||||||
|
overlayKey.currentState!.insert(entry);
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(OverlayEntry entry) {
|
||||||
|
if (entries.remove(entry)) {
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAll() {
|
||||||
|
for (var entry in entries) {
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Overlay(
|
||||||
|
key: overlayKey,
|
||||||
|
initialEntries: [OverlayEntry(builder: (context) => widget.child)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
300
lib/components/page_route.dart
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
|
||||||
|
const double _kBackGestureWidth = 20.0;
|
||||||
|
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
|
||||||
|
const int _kMaxPageBackAnimationTime = 300;
|
||||||
|
const double _kMinFlingVelocity = 1.0;
|
||||||
|
|
||||||
|
class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin {
|
||||||
|
/// Construct a MaterialPageRoute whose contents are defined by [builder].
|
||||||
|
AppPageRoute({
|
||||||
|
required this.builder,
|
||||||
|
super.settings,
|
||||||
|
this.maintainState = true,
|
||||||
|
super.fullscreenDialog,
|
||||||
|
super.allowSnapshotting = true,
|
||||||
|
super.barrierDismissible = false,
|
||||||
|
this.enableIOSGesture = true,
|
||||||
|
this.preventRebuild = true,
|
||||||
|
}) {
|
||||||
|
assert(opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the primary contents of the route.
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context) {
|
||||||
|
return builder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool maintainState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugLabel => '${super.debugLabel}(${settings.name})';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool enableIOSGesture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool preventRebuild;
|
||||||
|
|
||||||
|
static void updateBackButton() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
StateController.findOrNull(tag: "back_button")?.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
||||||
|
/// Builds the primary contents of the route.
|
||||||
|
@protected
|
||||||
|
Widget buildContent(BuildContext context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
|
||||||
|
// Don't perform outgoing animation if the next route is a fullscreen dialog.
|
||||||
|
return nextRoute is PageRoute && !nextRoute.fullscreenDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get enableIOSGesture;
|
||||||
|
|
||||||
|
bool get preventRebuild;
|
||||||
|
|
||||||
|
Widget? _child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
Widget result;
|
||||||
|
|
||||||
|
if (preventRebuild) {
|
||||||
|
result = _child ?? (_child = buildContent(context));
|
||||||
|
} else {
|
||||||
|
result = buildContent(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
scopesRoute: true,
|
||||||
|
explicitChildNodes: true,
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
|
||||||
|
if (route.isFirst ||
|
||||||
|
route.willHandlePopInternally ||
|
||||||
|
route.popDisposition == RoutePopDisposition.doNotPop ||
|
||||||
|
route.fullscreenDialog ||
|
||||||
|
route.animation!.status != AnimationStatus.completed ||
|
||||||
|
route.secondaryAnimation!.status != AnimationStatus.dismissed ||
|
||||||
|
route.navigator!.userGestureInProgress) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) {
|
||||||
|
return DrillInPageTransition(
|
||||||
|
animation: CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: FluentTheme.of(context).animationCurve,
|
||||||
|
),
|
||||||
|
child: enableIOSGesture
|
||||||
|
? IOSBackGestureDetector(
|
||||||
|
gestureWidth: _kBackGestureWidth,
|
||||||
|
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
||||||
|
onStartPopGesture: () => _startPopGesture(this),
|
||||||
|
child: child)
|
||||||
|
: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IOSBackGestureController _startPopGesture(PageRoute<T> route) {
|
||||||
|
return IOSBackGestureController(route.controller!, route.navigator!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IOSBackGestureController {
|
||||||
|
final AnimationController controller;
|
||||||
|
|
||||||
|
final NavigatorState navigator;
|
||||||
|
|
||||||
|
IOSBackGestureController(this.controller, this.navigator) {
|
||||||
|
navigator.didStartUserGesture();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dragEnd(double velocity) {
|
||||||
|
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
|
||||||
|
final bool animateForward;
|
||||||
|
|
||||||
|
if (velocity.abs() >= _kMinFlingVelocity) {
|
||||||
|
animateForward = velocity <= 0;
|
||||||
|
} else {
|
||||||
|
animateForward = controller.value > 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animateForward) {
|
||||||
|
final droppedPageForwardAnimationTime = min(
|
||||||
|
lerpDouble(
|
||||||
|
_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!
|
||||||
|
.floor(),
|
||||||
|
_kMaxPageBackAnimationTime,
|
||||||
|
);
|
||||||
|
controller.animateTo(1.0,
|
||||||
|
duration: Duration(milliseconds: droppedPageForwardAnimationTime),
|
||||||
|
curve: animationCurve);
|
||||||
|
} else {
|
||||||
|
navigator.pop();
|
||||||
|
if (controller.isAnimating) {
|
||||||
|
final droppedPageBackAnimationTime = lerpDouble(
|
||||||
|
0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!
|
||||||
|
.floor();
|
||||||
|
controller.animateBack(0.0,
|
||||||
|
duration: Duration(milliseconds: droppedPageBackAnimationTime),
|
||||||
|
curve: animationCurve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.isAnimating) {
|
||||||
|
late AnimationStatusListener animationStatusCallback;
|
||||||
|
animationStatusCallback = (status) {
|
||||||
|
navigator.didStopUserGesture();
|
||||||
|
controller.removeStatusListener(animationStatusCallback);
|
||||||
|
};
|
||||||
|
controller.addStatusListener(animationStatusCallback);
|
||||||
|
} else {
|
||||||
|
navigator.didStopUserGesture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dragUpdate(double delta) {
|
||||||
|
controller.value -= delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IOSBackGestureDetector extends StatefulWidget {
|
||||||
|
const IOSBackGestureDetector(
|
||||||
|
{required this.enabledCallback,
|
||||||
|
required this.child,
|
||||||
|
required this.gestureWidth,
|
||||||
|
required this.onStartPopGesture,
|
||||||
|
super.key});
|
||||||
|
|
||||||
|
final double gestureWidth;
|
||||||
|
|
||||||
|
final bool Function() enabledCallback;
|
||||||
|
|
||||||
|
final IOSBackGestureController Function() onStartPopGesture;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
|
||||||
|
IOSBackGestureController? _backGestureController;
|
||||||
|
|
||||||
|
late HorizontalDragGestureRecognizer _recognizer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_recognizer.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
|
||||||
|
..onStart = _handleDragStart
|
||||||
|
..onUpdate = _handleDragUpdate
|
||||||
|
..onEnd = _handleDragEnd
|
||||||
|
..onCancel = _handleDragCancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
|
||||||
|
? MediaQuery.of(context).padding.left
|
||||||
|
: MediaQuery.of(context).padding.right;
|
||||||
|
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
children: <Widget>[
|
||||||
|
widget.child,
|
||||||
|
Positioned(
|
||||||
|
width: dragAreaWidth,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 0,
|
||||||
|
child: Listener(
|
||||||
|
onPointerDown: _handlePointerDown,
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePointerDown(PointerDownEvent event) {
|
||||||
|
if (widget.enabledCallback()) _recognizer.addPointer(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragCancel() {
|
||||||
|
assert(mounted);
|
||||||
|
_backGestureController?.dragEnd(0.0);
|
||||||
|
_backGestureController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _convertToLogical(double value) {
|
||||||
|
switch (Directionality.of(context)) {
|
||||||
|
case TextDirection.rtl:
|
||||||
|
return -value;
|
||||||
|
case TextDirection.ltr:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
|
assert(mounted);
|
||||||
|
assert(_backGestureController != null);
|
||||||
|
_backGestureController!.dragEnd(_convertToLogical(
|
||||||
|
details.velocity.pixelsPerSecond.dx / context.size!.width));
|
||||||
|
_backGestureController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
assert(mounted);
|
||||||
|
assert(_backGestureController == null);
|
||||||
|
_backGestureController = widget.onStartPopGesture();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
|
assert(mounted);
|
||||||
|
assert(_backGestureController != null);
|
||||||
|
_backGestureController!.dragUpdate(
|
||||||
|
_convertToLogical(details.primaryDelta! / context.size!.width));
|
||||||
|
}
|
||||||
|
}
|
72
lib/components/segmented_button.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
|
||||||
|
import 'color_scheme.dart';
|
||||||
|
|
||||||
|
class SegmentedButton<T> extends StatelessWidget {
|
||||||
|
const SegmentedButton(
|
||||||
|
{required this.options,
|
||||||
|
required this.value,
|
||||||
|
required this.onPressed,
|
||||||
|
super.key});
|
||||||
|
|
||||||
|
final List<SegmentedButtonOption<T>> options;
|
||||||
|
|
||||||
|
final T value;
|
||||||
|
|
||||||
|
final void Function(T key) onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: options.map((e) => buildButton(e)).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildButton(SegmentedButtonOption<T> e) {
|
||||||
|
bool active = value == e.key;
|
||||||
|
return HoverButton(
|
||||||
|
cursor: active ? MouseCursor.defer : SystemMouseCursors.click,
|
||||||
|
onPressed: () => onPressed(e.key),
|
||||||
|
builder: (context, states) {
|
||||||
|
var textColor = active ? null : ColorScheme.of(context).outline;
|
||||||
|
var backgroundColor = active ? null : ButtonState.resolveWith((states) {
|
||||||
|
return ButtonThemeData.buttonColor(context, states);
|
||||||
|
}).resolve(states);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
border: e != options.last
|
||||||
|
? Border(
|
||||||
|
right: BorderSide(
|
||||||
|
width: 0.6,
|
||||||
|
color: ColorScheme.of(context).outlineVariant))
|
||||||
|
: null),
|
||||||
|
child: Center(
|
||||||
|
child: Text(e.text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: textColor, fontWeight: FontWeight.w500))
|
||||||
|
.paddingHorizontal(12),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SegmentedButtonOption<T> {
|
||||||
|
final T key;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const SegmentedButtonOption(this.key, this.text);
|
||||||
|
}
|
41
lib/foundation/app.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
export "widget_utils.dart";
|
||||||
|
export "state_controller.dart";
|
||||||
|
export "navigation.dart";
|
||||||
|
|
||||||
|
class _App {
|
||||||
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
bool get isIOS => Platform.isIOS;
|
||||||
|
bool get isWindows => Platform.isWindows;
|
||||||
|
bool get isLinux => Platform.isLinux;
|
||||||
|
bool get isMacOS => Platform.isMacOS;
|
||||||
|
bool get isDesktop =>
|
||||||
|
Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
||||||
|
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
||||||
|
|
||||||
|
Locale get locale {
|
||||||
|
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
||||||
|
if (deviceLocale.languageCode == "zh" && deviceLocale.scriptCode == "Hant") {
|
||||||
|
deviceLocale = const Locale("zh", "TW");
|
||||||
|
}
|
||||||
|
return deviceLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
late String dataPath;
|
||||||
|
late String cachePath;
|
||||||
|
|
||||||
|
init() async{
|
||||||
|
cachePath = (await getApplicationCacheDirectory()).path;
|
||||||
|
dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
final App = _App();
|
255
lib/foundation/cache_manager.dart
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:pixes/utils/io.dart';
|
||||||
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
|
|
||||||
|
import 'app.dart';
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
static String get cachePath => '${App.cachePath}/cache';
|
||||||
|
|
||||||
|
static CacheManager? instance;
|
||||||
|
|
||||||
|
late Database _db;
|
||||||
|
|
||||||
|
int? _currentSize;
|
||||||
|
|
||||||
|
/// size in bytes
|
||||||
|
int get currentSize => _currentSize ?? 0;
|
||||||
|
|
||||||
|
int dir = 0;
|
||||||
|
|
||||||
|
int _limitSize = 2 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
CacheManager._create(){
|
||||||
|
Directory(cachePath).createSync(recursive: true);
|
||||||
|
_db = sqlite3.open('${App.dataPath}/cache.db');
|
||||||
|
_db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS cache (
|
||||||
|
key TEXT PRIMARY KEY NOT NULL,
|
||||||
|
dir TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
expires INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
compute((path) => Directory(path).size, cachePath)
|
||||||
|
.then((value) => _currentSize = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CacheManager() => instance ??= CacheManager._create();
|
||||||
|
|
||||||
|
/// set cache size limit in bytes
|
||||||
|
void setLimitSize(int size){
|
||||||
|
_limitSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeCache(String key, Uint8List data, [int duration = 7 * 24 * 60 * 60 * 1000]) async{
|
||||||
|
this.dir++;
|
||||||
|
this.dir %= 100;
|
||||||
|
var dir = this.dir;
|
||||||
|
var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
while(await file.exists()){
|
||||||
|
name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();
|
||||||
|
file = File('$cachePath/$dir/$name');
|
||||||
|
}
|
||||||
|
await file.create(recursive: true);
|
||||||
|
await file.writeAsBytes(data);
|
||||||
|
var expires = DateTime.now().millisecondsSinceEpoch + duration;
|
||||||
|
_db.execute('''
|
||||||
|
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
|
||||||
|
''', [key, dir.toString(), name, expires]);
|
||||||
|
if(_currentSize != null) {
|
||||||
|
_currentSize = _currentSize! + data.length;
|
||||||
|
}
|
||||||
|
if(_currentSize != null && _currentSize! > _limitSize){
|
||||||
|
await checkCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CachingFile> openWrite(String key) async{
|
||||||
|
this.dir++;
|
||||||
|
this.dir %= 100;
|
||||||
|
var dir = this.dir;
|
||||||
|
var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
while(await file.exists()){
|
||||||
|
name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();
|
||||||
|
file = File('$cachePath/$dir/$name');
|
||||||
|
}
|
||||||
|
await file.create(recursive: true);
|
||||||
|
return CachingFile._(key, dir.toString(), name, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> findCache(String key) async{
|
||||||
|
var res = _db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
if(res.isEmpty){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var row = res.first;
|
||||||
|
var dir = row.values[1] as String;
|
||||||
|
var name = row.values[2] as String;
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
if(await file.exists()){
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isChecking = false;
|
||||||
|
|
||||||
|
Future<void> checkCache() async{
|
||||||
|
if(_isChecking){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isChecking = true;
|
||||||
|
var res = _db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
WHERE expires < ?
|
||||||
|
''', [DateTime.now().millisecondsSinceEpoch]);
|
||||||
|
for(var row in res){
|
||||||
|
var dir = row.values[1] as int;
|
||||||
|
var name = row.values[2] as String;
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
if(await file.exists()){
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE expires < ?
|
||||||
|
''', [DateTime.now().millisecondsSinceEpoch]);
|
||||||
|
|
||||||
|
while(_currentSize != null && _currentSize! > _limitSize){
|
||||||
|
var res = _db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
ORDER BY time ASC
|
||||||
|
limit 10
|
||||||
|
''');
|
||||||
|
for(var row in res){
|
||||||
|
var key = row.values[0] as String;
|
||||||
|
var dir = row.values[1] as int;
|
||||||
|
var name = row.values[2] as String;
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
if(await file.exists()){
|
||||||
|
var size = await file.length();
|
||||||
|
await file.delete();
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
_currentSize = _currentSize! - size;
|
||||||
|
if(_currentSize! <= _limitSize){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_isChecking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(String key) async{
|
||||||
|
var res = _db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
if(res.isEmpty){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var row = res.first;
|
||||||
|
var dir = row.values[1] as String;
|
||||||
|
var name = row.values[2] as String;
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
var fileSize = 0;
|
||||||
|
if(await file.exists()){
|
||||||
|
fileSize = await file.length();
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
if(_currentSize != null) {
|
||||||
|
_currentSize = _currentSize! - fileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
await Directory(cachePath).delete(recursive: true);
|
||||||
|
Directory(cachePath).createSync(recursive: true);
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
''');
|
||||||
|
_currentSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteKeyword(String keyword) async{
|
||||||
|
var res = _db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
WHERE key LIKE ?
|
||||||
|
''', ['%$keyword%']);
|
||||||
|
for(var row in res){
|
||||||
|
var key = row.values[0] as String;
|
||||||
|
var dir = row.values[1] as String;
|
||||||
|
var name = row.values[2] as String;
|
||||||
|
var file = File('$cachePath/$dir/$name');
|
||||||
|
var fileSize = 0;
|
||||||
|
if(await file.exists()){
|
||||||
|
fileSize = await file.length();
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
if(_currentSize != null) {
|
||||||
|
_currentSize = _currentSize! - fileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CachingFile{
|
||||||
|
CachingFile._(this.key, this.dir, this.name, this.file);
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
|
||||||
|
final String dir;
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
final File file;
|
||||||
|
|
||||||
|
final List<int> _buffer = [];
|
||||||
|
|
||||||
|
Future<void> writeBytes(List<int> data) async{
|
||||||
|
_buffer.addAll(data);
|
||||||
|
if(_buffer.length > 1024 * 1024){
|
||||||
|
await file.writeAsBytes(_buffer, mode: FileMode.append);
|
||||||
|
_buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> close() async{
|
||||||
|
if(_buffer.isNotEmpty){
|
||||||
|
await file.writeAsBytes(_buffer, mode: FileMode.append);
|
||||||
|
}
|
||||||
|
CacheManager()._db.execute('''
|
||||||
|
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
|
||||||
|
''', [key, dir, name, DateTime.now().millisecondsSinceEpoch + 7 * 24 * 60 * 60 * 1000]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancel() async{
|
||||||
|
await file.deleteIfExists();
|
||||||
|
}
|
||||||
|
}
|
193
lib/foundation/image_provider.dart
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui show Codec;
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:pixes/network/app_dio.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
|
||||||
|
import 'cache_manager.dart';
|
||||||
|
|
||||||
|
class BadRequestException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
BadRequestException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||||
|
extends ImageProvider<T> {
|
||||||
|
const BaseImageProvider();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
|
||||||
|
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _loadBufferAsync(key, chunkEvents, decode),
|
||||||
|
chunkEvents: chunkEvents.stream,
|
||||||
|
scale: 1.0,
|
||||||
|
informationCollector: () sync* {
|
||||||
|
yield DiagnosticsProperty<ImageProvider>(
|
||||||
|
'Image provider: $this \n Image key: $key',
|
||||||
|
this,
|
||||||
|
style: DiagnosticsTreeStyle.errorProperty,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Codec> _loadBufferAsync(
|
||||||
|
T key,
|
||||||
|
StreamController<ImageChunkEvent> chunkEvents,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
int retryTime = 1;
|
||||||
|
|
||||||
|
bool stop = false;
|
||||||
|
|
||||||
|
chunkEvents.onCancel = () {
|
||||||
|
stop = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Uint8List? data;
|
||||||
|
|
||||||
|
while (data == null && !stop) {
|
||||||
|
try {
|
||||||
|
data = await load(chunkEvents);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains("Your IP address")) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
if (e is BadRequestException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
if (e.toString().contains("handshake")) {
|
||||||
|
if (retryTime < 5) {
|
||||||
|
retryTime = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retryTime <<= 1;
|
||||||
|
if (retryTime > (2 << 3) || stop) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
await Future.delayed(Duration(seconds: retryTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stop) {
|
||||||
|
throw Exception("Image loading is stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(data!.isEmpty) {
|
||||||
|
throw Exception("Empty image data");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final buffer = await ImmutableBuffer.fromUint8List(data);
|
||||||
|
return await decode(buffer);
|
||||||
|
} catch (e) {
|
||||||
|
await CacheManager().delete(this.key);
|
||||||
|
Object error = e;
|
||||||
|
if (data.length < 200) {
|
||||||
|
// data is too short, it's likely that the data is text, not image
|
||||||
|
try {
|
||||||
|
var text = utf8.decoder.convert(data);
|
||||||
|
error = Exception("Expected image data, but got text: $text");
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
|
});
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
chunkEvents.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents);
|
||||||
|
|
||||||
|
String get key;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is BaseImageProvider<T> && key == other.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => key.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "$runtimeType($key)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);
|
||||||
|
|
||||||
|
class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
CachedImageProvider(this.url);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => url;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async{
|
||||||
|
var cached = await CacheManager().findCache(key);
|
||||||
|
if(cached != null) {
|
||||||
|
return await File(cached).readAsBytes();
|
||||||
|
}
|
||||||
|
var dio = AppDio();
|
||||||
|
final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
|
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
||||||
|
var res = await dio.get(
|
||||||
|
url,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
validateStatus: (status) => status != null && status < 500,
|
||||||
|
headers: {
|
||||||
|
"referer": "https://app-api.pixiv.net/",
|
||||||
|
"user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)",
|
||||||
|
"x-client-time": time,
|
||||||
|
"x-client-hash": hash,
|
||||||
|
"accept-enconding": "gzip",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if(res.statusCode != 200) {
|
||||||
|
throw BadRequestException("Failed to load image: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
var data = <int>[];
|
||||||
|
var cachingFile = await CacheManager().openWrite(key);
|
||||||
|
await for (var chunk in res.data.stream) {
|
||||||
|
data.addAll(chunk);
|
||||||
|
await cachingFile.writeBytes(chunk);
|
||||||
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: data.length,
|
||||||
|
expectedTotalBytes: res.data.contentLength+1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
await cachingFile.close();
|
||||||
|
return Uint8List.fromList(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CachedImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture<CachedImageProvider>(this);
|
||||||
|
}
|
||||||
|
}
|
91
lib/foundation/log.dart
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:pixes/utils/ext.dart';
|
||||||
|
|
||||||
|
class LogItem {
|
||||||
|
final LogLevel level;
|
||||||
|
final String title;
|
||||||
|
final String content;
|
||||||
|
final DateTime time = DateTime.now();
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "${level.name} $title $time \n$content\n\n";
|
||||||
|
|
||||||
|
LogItem(this.level, this.title, this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LogLevel { error, warning, info }
|
||||||
|
|
||||||
|
class Log {
|
||||||
|
static final List<LogItem> _logs = <LogItem>[];
|
||||||
|
|
||||||
|
static List<LogItem> get logs => _logs;
|
||||||
|
|
||||||
|
static const maxLogLength = 3000;
|
||||||
|
|
||||||
|
static const maxLogNumber = 500;
|
||||||
|
|
||||||
|
static bool ignoreLimitation = false;
|
||||||
|
|
||||||
|
static void printWarning(String text) {
|
||||||
|
print('\x1B[33m$text\x1B[0m');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void printError(String text) {
|
||||||
|
print('\x1B[31m$text\x1B[0m');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addLog(LogLevel level, String title, String content) {
|
||||||
|
if (!ignoreLimitation && content.length > maxLogLength) {
|
||||||
|
content = "${content.substring(0, maxLogLength)}...";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.error:
|
||||||
|
printError(content);
|
||||||
|
case LogLevel.warning:
|
||||||
|
printWarning(content);
|
||||||
|
case LogLevel.info:
|
||||||
|
print(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newLog = LogItem(level, title, content);
|
||||||
|
|
||||||
|
if (newLog == _logs.lastOrNull) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logs.add(newLog);
|
||||||
|
if (_logs.length > maxLogNumber) {
|
||||||
|
var res = _logs.remove(
|
||||||
|
_logs.firstWhereOrNull((element) => element.level == LogLevel.info));
|
||||||
|
if (!res) {
|
||||||
|
_logs.removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static info(String title, String content) {
|
||||||
|
addLog(LogLevel.info, title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
static warning(String title, String content) {
|
||||||
|
addLog(LogLevel.warning, title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
static error(String title, String content) {
|
||||||
|
addLog(LogLevel.error, title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear() => _logs.clear();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
var res = "Logs\n\n";
|
||||||
|
for (var log in _logs) {
|
||||||
|
res += log.toString();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
19
lib/foundation/navigation.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
|
import '../components/message.dart' as overlay;
|
||||||
|
import '../components/page_route.dart';
|
||||||
|
|
||||||
|
extension Navigation on BuildContext {
|
||||||
|
void pop<T>([T? result]) {
|
||||||
|
Navigator.of(this).pop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> to<T>(Widget Function() builder) {
|
||||||
|
return Navigator.of(this)
|
||||||
|
.push<T>(AppPageRoute(builder: (context) => builder()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void showToast({required String message, IconData? icon}) {
|
||||||
|
overlay.showToast(this, message: message, icon: icon);
|
||||||
|
}
|
||||||
|
}
|
9
lib/foundation/pair.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Pair<M, V>{
|
||||||
|
M left;
|
||||||
|
V right;
|
||||||
|
|
||||||
|
Pair(this.left, this.right);
|
||||||
|
|
||||||
|
Pair.fromMap(Map<M, V> map, M key): left = key, right = map[key]
|
||||||
|
?? (throw Exception("Pair not found"));
|
||||||
|
}
|
195
lib/foundation/state_controller.dart
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'pair.dart';
|
||||||
|
|
||||||
|
class SimpleController extends StateController{
|
||||||
|
final void Function()? refresh_;
|
||||||
|
|
||||||
|
SimpleController({this.refresh_});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void refresh() {
|
||||||
|
(refresh_ ?? super.refresh)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class StateController{
|
||||||
|
static final _controllers = <StateControllerWrapped>[];
|
||||||
|
|
||||||
|
static T put<T extends StateController>(T controller, {Object? tag, bool autoRemove = false}){
|
||||||
|
_controllers.add(StateControllerWrapped(controller, autoRemove, tag));
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
static T putIfNotExists<T extends StateController>(T controller, {Object? tag, bool autoRemove = false}){
|
||||||
|
return findOrNull<T>(tag: tag) ?? put(controller, tag: tag, autoRemove: autoRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
static T find<T extends StateController>({Object? tag}){
|
||||||
|
try {
|
||||||
|
return _controllers.lastWhere((element) =>
|
||||||
|
element.controller is T
|
||||||
|
&& (tag == null || tag == element.tag)).controller as T;
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw StateError("${T.runtimeType} with tag $tag Not Found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static T? findOrNull<T extends StateController>({Object? tag}){
|
||||||
|
try {
|
||||||
|
return _controllers.lastWhere((element) =>
|
||||||
|
element.controller is T
|
||||||
|
&& (tag == null || tag == element.tag)).controller as T;
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove<T>([Object? tag, bool check = false]){
|
||||||
|
for(int i=_controllers.length-1; i>=0; i--){
|
||||||
|
var element = _controllers[i];
|
||||||
|
if(element.controller is T && (tag == null || tag == element.tag)){
|
||||||
|
if(check && !element.autoRemove){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_controllers.removeAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SimpleController putSimpleController(void Function() onUpdate, Object? tag, {void Function()? refresh}){
|
||||||
|
var controller = SimpleController(refresh_: refresh);
|
||||||
|
controller.stateUpdaters.add(Pair(null, onUpdate));
|
||||||
|
_controllers.add(StateControllerWrapped(controller, false, tag));
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<Object?, void Function()>> stateUpdaters = [];
|
||||||
|
|
||||||
|
void update([List<Object>? ids]){
|
||||||
|
if(ids == null){
|
||||||
|
for(var element in stateUpdaters){
|
||||||
|
element.right();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
for(var element in stateUpdaters){
|
||||||
|
if(ids.contains(element.left)) {
|
||||||
|
element.right();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose(){
|
||||||
|
_controllers.removeWhere((element) => element.controller == this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh(){
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateControllerWrapped{
|
||||||
|
StateController controller;
|
||||||
|
bool autoRemove;
|
||||||
|
Object? tag;
|
||||||
|
|
||||||
|
StateControllerWrapped(this.controller, this.autoRemove, this.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StateBuilder<T extends StateController> extends StatefulWidget {
|
||||||
|
const StateBuilder({super.key, this.init, this.dispose, this.initState, this.tag,
|
||||||
|
required this.builder, this.id});
|
||||||
|
|
||||||
|
final T? init;
|
||||||
|
|
||||||
|
final void Function(T controller)? dispose;
|
||||||
|
|
||||||
|
final void Function(T controller)? initState;
|
||||||
|
|
||||||
|
final Object? tag;
|
||||||
|
|
||||||
|
final Widget Function(T controller) builder;
|
||||||
|
|
||||||
|
Widget builderWrapped(StateController controller){
|
||||||
|
return builder(controller as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initStateWrapped(StateController controller){
|
||||||
|
return initState?.call(controller as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeWrapped(StateController controller){
|
||||||
|
return dispose?.call(controller as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object? id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StateBuilder> createState() => _StateBuilderState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StateBuilderState<T extends StateController> extends State<StateBuilder> {
|
||||||
|
late T controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
if(widget.init != null) {
|
||||||
|
StateController.put(widget.init!, tag: widget.tag, autoRemove: true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
controller = StateController.find<T>(tag: widget.tag);
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw "Controller Not Found";
|
||||||
|
}
|
||||||
|
controller.stateUpdaters.add(Pair(widget.id, () {
|
||||||
|
if(mounted){
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
widget.initStateWrapped(controller);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.disposeWrapped(controller);
|
||||||
|
StateController.remove<T>(widget.tag, true);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.builderWrapped(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class StateWithController<T extends StatefulWidget> extends State<T>{
|
||||||
|
late final SimpleController _controller;
|
||||||
|
|
||||||
|
void refresh(){
|
||||||
|
_controller.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
void initState() {
|
||||||
|
_controller = StateController.putSimpleController(() => setState(() {}), tag, refresh: refresh);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(){
|
||||||
|
_controller.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? get tag;
|
||||||
|
}
|
59
lib/foundation/widget_utils.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
extension WidgetExtension on Widget{
|
||||||
|
Widget padding(EdgeInsetsGeometry padding){
|
||||||
|
return Padding(padding: padding, child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingLeft(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.only(left: padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingRight(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.only(right: padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingTop(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.only(top: padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingBottom(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.only(bottom: padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingVertical(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.symmetric(vertical: padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingHorizontal(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.symmetric(horizontal: padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget paddingAll(double padding){
|
||||||
|
return Padding(padding: EdgeInsets.all(padding), child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget toCenter(){
|
||||||
|
return Center(child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget toAlign(AlignmentGeometry alignment){
|
||||||
|
return Align(alignment: alignment, child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget sliverPadding(EdgeInsetsGeometry padding){
|
||||||
|
return SliverPadding(padding: padding, sliver: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget sliverPaddingAll(double padding){
|
||||||
|
return SliverPadding(padding: EdgeInsets.all(padding), sliver: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget sliverPaddingVertical(double padding){
|
||||||
|
return SliverPadding(padding: EdgeInsets.symmetric(vertical: padding), sliver: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget sliverPaddingHorizontal(double padding){
|
||||||
|
return SliverPadding(padding: EdgeInsets.symmetric(horizontal: padding), sliver: this);
|
||||||
|
}
|
||||||
|
}
|
95
lib/main.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import "package:fluent_ui/fluent_ui.dart";
|
||||||
|
import "package:pixes/appdata.dart";
|
||||||
|
import "package:pixes/components/message.dart";
|
||||||
|
import "package:pixes/foundation/app.dart";
|
||||||
|
import "package:pixes/foundation/log.dart";
|
||||||
|
import "package:pixes/network/app_dio.dart";
|
||||||
|
import "package:pixes/pages/main_page.dart";
|
||||||
|
import "package:pixes/utils/app_links.dart";
|
||||||
|
import "package:window_manager/window_manager.dart";
|
||||||
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
FlutterError.onError = (details) {
|
||||||
|
Log.error("Unhandled", "${details.exception}\n${details.stack}");
|
||||||
|
};
|
||||||
|
setSystemProxy();
|
||||||
|
SystemTheme.fallbackColor = Colors.blue;
|
||||||
|
await SystemTheme.accentColor.load();
|
||||||
|
await App.init();
|
||||||
|
await appdata.readData();
|
||||||
|
handleLinks();
|
||||||
|
SystemTheme.onChange.listen((event) {
|
||||||
|
StateController.findOrNull(tag: "MyApp")?.update();
|
||||||
|
});
|
||||||
|
if (App.isDesktop) {
|
||||||
|
await WindowManager.instance.ensureInitialized();
|
||||||
|
windowManager.waitUntilReadyToShow().then((_) async {
|
||||||
|
await windowManager.setTitleBarStyle(
|
||||||
|
TitleBarStyle.hidden,
|
||||||
|
windowButtonVisibility: false,
|
||||||
|
);
|
||||||
|
await windowManager.setMinimumSize(const Size(500, 600));
|
||||||
|
await windowManager.show();
|
||||||
|
await windowManager.setSkipTaskbar(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StateBuilder<SimpleController>(
|
||||||
|
init: SimpleController(),
|
||||||
|
tag: "MyApp",
|
||||||
|
builder: (controller) {
|
||||||
|
return FluentApp(
|
||||||
|
navigatorKey: App.rootNavigatorKey,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
title: 'pixes',
|
||||||
|
theme: FluentThemeData(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
accentColor: AccentColor.swatch({
|
||||||
|
'darkest': SystemTheme.accentColor.darkest,
|
||||||
|
'darker': SystemTheme.accentColor.darker,
|
||||||
|
'dark': SystemTheme.accentColor.dark,
|
||||||
|
'normal': SystemTheme.accentColor.accent,
|
||||||
|
'light': SystemTheme.accentColor.light,
|
||||||
|
'lighter': SystemTheme.accentColor.lighter,
|
||||||
|
'lightest': SystemTheme.accentColor.lightest,
|
||||||
|
})),
|
||||||
|
darkTheme: FluentThemeData(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
accentColor: AccentColor.swatch({
|
||||||
|
'darkest': SystemTheme.accentColor.darkest,
|
||||||
|
'darker': SystemTheme.accentColor.darker,
|
||||||
|
'dark': SystemTheme.accentColor.dark,
|
||||||
|
'normal': SystemTheme.accentColor.accent,
|
||||||
|
'light': SystemTheme.accentColor.light,
|
||||||
|
'lighter': SystemTheme.accentColor.lighter,
|
||||||
|
'lightest': SystemTheme.accentColor.lightest,
|
||||||
|
})),
|
||||||
|
home: const MainPage(),
|
||||||
|
builder: (context, child) {
|
||||||
|
ErrorWidget.builder = (details) {
|
||||||
|
if (details.exception
|
||||||
|
.toString()
|
||||||
|
.contains("RenderFlex overflowed")) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
Log.error("UI", "${details.exception}\n${details.stack}");
|
||||||
|
return Text(details.exception.toString());
|
||||||
|
};
|
||||||
|
if (child == null) {
|
||||||
|
throw "widget is null";
|
||||||
|
}
|
||||||
|
|
||||||
|
return OverlayWidget(child);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
140
lib/network/app_dio.dart
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:pixes/foundation/log.dart';
|
||||||
|
|
||||||
|
export 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class MyLogInterceptor implements Interceptor {
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
|
Log.error("Network",
|
||||||
|
"${err.requestOptions.method} ${err.requestOptions.path}\n$err\n${err.response?.data.toString()}");
|
||||||
|
switch (err.type) {
|
||||||
|
case DioExceptionType.badResponse:
|
||||||
|
var statusCode = err.response?.statusCode;
|
||||||
|
if (statusCode != null) {
|
||||||
|
err = err.copyWith(
|
||||||
|
message: "Invalid Status Code: $statusCode. "
|
||||||
|
"${_getStatusCodeInfo(statusCode)}");
|
||||||
|
}
|
||||||
|
case DioExceptionType.connectionTimeout:
|
||||||
|
err = err.copyWith(message: "Connection Timeout");
|
||||||
|
case DioExceptionType.receiveTimeout:
|
||||||
|
err = err.copyWith(
|
||||||
|
message: "Receive Timeout: "
|
||||||
|
"This indicates that the server is too busy to respond");
|
||||||
|
case DioExceptionType.unknown:
|
||||||
|
if (err.toString().contains("Connection terminated during handshake")) {
|
||||||
|
err = err.copyWith(
|
||||||
|
message: "Connection terminated during handshake: "
|
||||||
|
"This may be caused by the firewall blocking the connection "
|
||||||
|
"or your requests are too frequent.");
|
||||||
|
} else if (err.toString().contains("Connection reset by peer")) {
|
||||||
|
err = err.copyWith(
|
||||||
|
message: "Connection reset by peer: "
|
||||||
|
"The error is unrelated to app, please check your network.");
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const errorMessages = <int, String>{
|
||||||
|
400: "The Request is invalid.",
|
||||||
|
401: "The Request is unauthorized.",
|
||||||
|
403: "No permission to access the resource. Check your account or network.",
|
||||||
|
404: "Not found.",
|
||||||
|
429: "Too many requests. Please try again later.",
|
||||||
|
};
|
||||||
|
|
||||||
|
String _getStatusCodeInfo(int? statusCode) {
|
||||||
|
if (statusCode != null && statusCode >= 500) {
|
||||||
|
return "This is server-side error, please try again later. "
|
||||||
|
"Do not report this issue.";
|
||||||
|
} else {
|
||||||
|
return errorMessages[statusCode] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onResponse(
|
||||||
|
Response<dynamic> response, ResponseInterceptorHandler handler) {
|
||||||
|
var headers = response.headers.map.map((key, value) => MapEntry(
|
||||||
|
key.toLowerCase(), value.length == 1 ? value.first : value.toString()));
|
||||||
|
headers.remove("cookie");
|
||||||
|
String content;
|
||||||
|
if (response.data is List<int>) {
|
||||||
|
content = "<Bytes>\nlength:${response.data.length}";
|
||||||
|
} else {
|
||||||
|
content = response.data.toString();
|
||||||
|
}
|
||||||
|
Log.addLog(
|
||||||
|
(response.statusCode != null && response.statusCode! < 400)
|
||||||
|
? LogLevel.info
|
||||||
|
: LogLevel.error,
|
||||||
|
"Network",
|
||||||
|
"Response ${response.realUri.toString()} ${response.statusCode}\n"
|
||||||
|
"headers:\n$headers\n$content");
|
||||||
|
handler.next(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
|
options.connectTimeout = const Duration(seconds: 15);
|
||||||
|
options.receiveTimeout = const Duration(seconds: 15);
|
||||||
|
options.sendTimeout = const Duration(seconds: 15);
|
||||||
|
if (options.headers["Host"] == null && options.headers["host"] == null) {
|
||||||
|
options.headers["host"] = options.uri.host;
|
||||||
|
}
|
||||||
|
Log.info("Network",
|
||||||
|
"${options.method} ${options.uri}\n${options.headers}\n${options.data}");
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDio extends DioForNative {
|
||||||
|
bool isInitialized = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response<T>> request<T>(String path,
|
||||||
|
{Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
Options? options,
|
||||||
|
ProgressCallback? onSendProgress,
|
||||||
|
ProgressCallback? onReceiveProgress}) {
|
||||||
|
if (!isInitialized) {
|
||||||
|
isInitialized = true;
|
||||||
|
interceptors.add(MyLogInterceptor());
|
||||||
|
}
|
||||||
|
return super.request(path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
options: options,
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
onReceiveProgress: onReceiveProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSystemProxy() {
|
||||||
|
HttpOverrides.global = _ProxyHttpOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProxyHttpOverrides extends HttpOverrides {
|
||||||
|
String findProxy(Uri uri) {
|
||||||
|
// TODO: proxy
|
||||||
|
return "DIRECT";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpClient createHttpClient(SecurityContext? context) {
|
||||||
|
final client = super.createHttpClient(context);
|
||||||
|
client.connectionTimeout = const Duration(seconds: 5);
|
||||||
|
client.findProxy = findProxy;
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
5
lib/network/download.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
|
||||||
|
extension IllustExt on Illust {
|
||||||
|
bool get downloaded => false;
|
||||||
|
}
|
215
lib/network/models.dart
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
class Account {
|
||||||
|
String accessToken;
|
||||||
|
String refreshToken;
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
Account(this.accessToken, this.refreshToken, this.user);
|
||||||
|
|
||||||
|
Account.fromJson(Map<String, dynamic> json)
|
||||||
|
: accessToken = json['access_token'],
|
||||||
|
refreshToken = json['refresh_token'],
|
||||||
|
user = User.fromJson(json['user']);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'access_token': accessToken,
|
||||||
|
'refresh_token': refreshToken,
|
||||||
|
'user': user.toJson()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
String profile;
|
||||||
|
final String id;
|
||||||
|
String name;
|
||||||
|
String account;
|
||||||
|
String email;
|
||||||
|
bool isPremium;
|
||||||
|
|
||||||
|
User(this.profile, this.id, this.name, this.account, this.email,
|
||||||
|
this.isPremium);
|
||||||
|
|
||||||
|
User.fromJson(Map<String, dynamic> json)
|
||||||
|
: profile = json['profile_image_urls']['px_170x170'],
|
||||||
|
id = json['id'],
|
||||||
|
name = json['name'],
|
||||||
|
account = json['account'],
|
||||||
|
email = json['mail_address'],
|
||||||
|
isPremium = json['is_premium'];
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'profile_image_urls': {'px_170x170': profile},
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'account': account,
|
||||||
|
'mail_address': email,
|
||||||
|
'is_premium': isPremium
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserDetails {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String account;
|
||||||
|
final String avatar;
|
||||||
|
final String comment;
|
||||||
|
final bool isFollowed;
|
||||||
|
final bool isBlocking;
|
||||||
|
final String? webpage;
|
||||||
|
final String gender;
|
||||||
|
final String birth;
|
||||||
|
final String region;
|
||||||
|
final String job;
|
||||||
|
final int totalFollowUsers;
|
||||||
|
final int myPixivUsers;
|
||||||
|
final int totalIllusts;
|
||||||
|
final int totalMangas;
|
||||||
|
final int totalNovels;
|
||||||
|
final int totalIllustBookmarks;
|
||||||
|
final String? backgroundImage;
|
||||||
|
final String? twitterUrl;
|
||||||
|
final bool isPremium;
|
||||||
|
final String? pawooUrl;
|
||||||
|
|
||||||
|
UserDetails(
|
||||||
|
this.id,
|
||||||
|
this.name,
|
||||||
|
this.account,
|
||||||
|
this.avatar,
|
||||||
|
this.comment,
|
||||||
|
this.isFollowed,
|
||||||
|
this.isBlocking,
|
||||||
|
this.webpage,
|
||||||
|
this.gender,
|
||||||
|
this.birth,
|
||||||
|
this.region,
|
||||||
|
this.job,
|
||||||
|
this.totalFollowUsers,
|
||||||
|
this.myPixivUsers,
|
||||||
|
this.totalIllusts,
|
||||||
|
this.totalMangas,
|
||||||
|
this.totalNovels,
|
||||||
|
this.totalIllustBookmarks,
|
||||||
|
this.backgroundImage,
|
||||||
|
this.twitterUrl,
|
||||||
|
this.isPremium,
|
||||||
|
this.pawooUrl);
|
||||||
|
|
||||||
|
UserDetails.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['user']['id'],
|
||||||
|
name = json['user']['name'],
|
||||||
|
account = json['user']['account'],
|
||||||
|
avatar = json['user']['profile_image_urls']['medium'],
|
||||||
|
comment = json['user']['comment'],
|
||||||
|
isFollowed = json['user']['is_followed'],
|
||||||
|
isBlocking = json['user']['is_access_blocking_user'],
|
||||||
|
webpage = json['profile']['webpage'],
|
||||||
|
gender = json['profile']['gender'],
|
||||||
|
birth = json['profile']['birth'],
|
||||||
|
region = json['profile']['region'],
|
||||||
|
job = json['profile']['job'],
|
||||||
|
totalFollowUsers = json['profile']['total_follow_users'],
|
||||||
|
myPixivUsers = json['profile']['total_mypixiv_users'],
|
||||||
|
totalIllusts = json['profile']['total_illusts'],
|
||||||
|
totalMangas = json['profile']['total_manga'],
|
||||||
|
totalNovels = json['profile']['total_novels'],
|
||||||
|
totalIllustBookmarks = json['profile']['total_illust_bookmarks_public'],
|
||||||
|
backgroundImage = json['profile']['background_image_url'],
|
||||||
|
twitterUrl = json['profile']['twitter_url'],
|
||||||
|
isPremium = json['profile']['is_premium'],
|
||||||
|
pawooUrl = json['profile']['pawoo_url'];
|
||||||
|
}
|
||||||
|
|
||||||
|
class IllustAuthor {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String account;
|
||||||
|
final String avatar;
|
||||||
|
bool isFollowed;
|
||||||
|
|
||||||
|
IllustAuthor(
|
||||||
|
this.id, this.name, this.account, this.avatar, this.isFollowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tag {
|
||||||
|
final String name;
|
||||||
|
final String? translatedName;
|
||||||
|
|
||||||
|
const Tag(this.name, this.translatedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IllustImage {
|
||||||
|
final String squareMedium;
|
||||||
|
final String medium;
|
||||||
|
final String large;
|
||||||
|
final String original;
|
||||||
|
|
||||||
|
const IllustImage(this.squareMedium, this.medium, this.large, this.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Illust {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String type;
|
||||||
|
final List<IllustImage> images;
|
||||||
|
final String caption;
|
||||||
|
final int restrict;
|
||||||
|
final IllustAuthor author;
|
||||||
|
final List<Tag> tags;
|
||||||
|
final String createDate;
|
||||||
|
final int pageCount;
|
||||||
|
final int width;
|
||||||
|
final int height;
|
||||||
|
final int totalView;
|
||||||
|
final int totalBookmarks;
|
||||||
|
bool isBookmarked;
|
||||||
|
final bool isAi;
|
||||||
|
|
||||||
|
Illust.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['id'],
|
||||||
|
title = json['title'],
|
||||||
|
type = json['type'],
|
||||||
|
images = (() {
|
||||||
|
List<IllustImage> images = [];
|
||||||
|
for (var i in json['meta_pages']) {
|
||||||
|
images.add(IllustImage(
|
||||||
|
i['image_urls']['square_medium'],
|
||||||
|
i['image_urls']['medium'],
|
||||||
|
i['image_urls']['large'],
|
||||||
|
i['image_urls']['original']));
|
||||||
|
}
|
||||||
|
if (images.isEmpty) {
|
||||||
|
images.add(IllustImage(
|
||||||
|
json['image_urls']['square_medium'],
|
||||||
|
json['image_urls']['medium'],
|
||||||
|
json['image_urls']['large'],
|
||||||
|
json['meta_single_page']['original_image_url']));
|
||||||
|
}
|
||||||
|
return images;
|
||||||
|
}()),
|
||||||
|
caption = json['caption'],
|
||||||
|
restrict = json['restrict'],
|
||||||
|
author = IllustAuthor(
|
||||||
|
json['user']['id'],
|
||||||
|
json['user']['name'],
|
||||||
|
json['user']['account'],
|
||||||
|
json['user']['profile_image_urls']['medium'],
|
||||||
|
json['user']['is_followed'] ?? false),
|
||||||
|
tags = (json['tags'] as List)
|
||||||
|
.map((e) => Tag(e['name'], e['translated_name']))
|
||||||
|
.toList(),
|
||||||
|
createDate = json['create_date'],
|
||||||
|
pageCount = json['page_count'],
|
||||||
|
width = json['width'],
|
||||||
|
height = json['height'],
|
||||||
|
totalView = json['total_view'],
|
||||||
|
totalBookmarks = json['total_bookmarks'],
|
||||||
|
isBookmarked = json['is_bookmarked'],
|
||||||
|
isAi = json['is_ai'] != 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrendingTag {
|
||||||
|
final Tag tag;
|
||||||
|
final Illust illust;
|
||||||
|
|
||||||
|
TrendingTag(this.tag, this.illust);
|
||||||
|
}
|
258
lib/network/network.dart
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:pixes/appdata.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/log.dart';
|
||||||
|
import 'package:pixes/network/app_dio.dart';
|
||||||
|
import 'package:pixes/network/res.dart';
|
||||||
|
|
||||||
|
import 'models.dart';
|
||||||
|
|
||||||
|
export 'models.dart';
|
||||||
|
export 'res.dart';
|
||||||
|
|
||||||
|
class Network {
|
||||||
|
static const hashSalt =
|
||||||
|
"28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
|
||||||
|
|
||||||
|
static const baseUrl = 'https://app-api.pixiv.net';
|
||||||
|
static const oauthUrl = 'https://oauth.secure.pixiv.net';
|
||||||
|
|
||||||
|
static const String clientID = "MOBrBDS8blbauoSck0ZfDbtuzpyT";
|
||||||
|
static const String clientSecret = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj";
|
||||||
|
static const String refreshClientID = "KzEZED7aC0vird8jWyHM38mXjNTY";
|
||||||
|
static const String refreshClientSecret =
|
||||||
|
"W9JZoJe00qPvJsiyCGT3CCtC6ZUtdpKpzMbNlUGP";
|
||||||
|
|
||||||
|
static Network? instance;
|
||||||
|
|
||||||
|
factory Network() => instance ?? (instance = Network._create());
|
||||||
|
|
||||||
|
Network._create();
|
||||||
|
|
||||||
|
String? codeVerifier;
|
||||||
|
|
||||||
|
String? get token => appdata.account?.accessToken;
|
||||||
|
|
||||||
|
final dio = AppDio();
|
||||||
|
|
||||||
|
Map<String, String> get _headers {
|
||||||
|
final time =
|
||||||
|
DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
|
final hash = md5.convert(utf8.encode(time + hashSalt)).toString();
|
||||||
|
return {
|
||||||
|
"X-Client-Time": time,
|
||||||
|
"X-Client-Hash": hash,
|
||||||
|
"User-Agent": "PixivAndroidApp/5.0.234 (Android 14.0; Pixes)",
|
||||||
|
"accept-language": App.locale.toLanguageTag(),
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
if (token != null) "Authorization": "Bearer $token"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> generateWebviewUrl() async {
|
||||||
|
const String chars =
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||||
|
codeVerifier =
|
||||||
|
List.generate(128, (i) => chars[Random.secure().nextInt(chars.length)])
|
||||||
|
.join();
|
||||||
|
final codeChallenge = base64Url
|
||||||
|
.encode(sha256.convert(ascii.encode(codeVerifier!)).bytes)
|
||||||
|
.replaceAll('=', '');
|
||||||
|
return "https://app-api.pixiv.net/web/v1/login?code_challenge=$codeChallenge&code_challenge_method=S256&client=pixiv-android";
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> loginWithCode(String code) async {
|
||||||
|
try {
|
||||||
|
var res = await dio.post<String>("$oauthUrl/auth/token",
|
||||||
|
data: {
|
||||||
|
"client_id": clientID,
|
||||||
|
"client_secret": clientSecret,
|
||||||
|
"code": code,
|
||||||
|
"code_verifier": codeVerifier,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"include_policy": "true",
|
||||||
|
"redirect_uri":
|
||||||
|
"https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback",
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
headers: _headers));
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw "Invalid Status code ${res.statusCode}";
|
||||||
|
}
|
||||||
|
final data = json.decode(res.data!);
|
||||||
|
appdata.account = Account.fromJson(data);
|
||||||
|
appdata.writeData();
|
||||||
|
return const Res(true);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> refreshToken() async {
|
||||||
|
try {
|
||||||
|
var res = await dio.post<String>("$oauthUrl/auth/token",
|
||||||
|
data: {
|
||||||
|
"client_id": clientID,
|
||||||
|
"client_secret": clientSecret,
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": appdata.account?.refreshToken,
|
||||||
|
"include_policy": "true",
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
headers: _headers));
|
||||||
|
var account = Account.fromJson(json.decode(res.data!));
|
||||||
|
appdata.account = account;
|
||||||
|
appdata.writeData();
|
||||||
|
return const Res(true);
|
||||||
|
}
|
||||||
|
catch(e, s){
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<Map<String, dynamic>>> apiGet(String path, {Map<String, dynamic>? query}) async {
|
||||||
|
try {
|
||||||
|
if(!path.startsWith("http")) {
|
||||||
|
path = "$baseUrl$path";
|
||||||
|
}
|
||||||
|
final res = await dio.get<Map<String, dynamic>>(path,
|
||||||
|
queryParameters: query, options: Options(headers: _headers, validateStatus: (status) => true));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return Res(res.data!);
|
||||||
|
} else if(res.statusCode == 400) {
|
||||||
|
if(res.data.toString().contains("Access Token")) {
|
||||||
|
var refresh = await refreshToken();
|
||||||
|
if(refresh.success) {
|
||||||
|
return apiGet(path, query: query);
|
||||||
|
} else {
|
||||||
|
return Res.error(refresh.errorMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Res.error("Invalid Status Code: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
} else if((res.statusCode??500) < 500){
|
||||||
|
return Res.error(res.data?["error"]?["message"] ?? "Invalid Status code ${res.statusCode}");
|
||||||
|
} else {
|
||||||
|
return Res.error("Invalid Status Code: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<Map<String, dynamic>>> apiPost(String path, {Map<String, dynamic>? query, Map<String, dynamic>? data}) async {
|
||||||
|
try {
|
||||||
|
if(!path.startsWith("http")) {
|
||||||
|
path = "$baseUrl$path";
|
||||||
|
}
|
||||||
|
final res = await dio.post<Map<String, dynamic>>(path,
|
||||||
|
queryParameters: query,
|
||||||
|
data: data,
|
||||||
|
options: Options(
|
||||||
|
headers: _headers,
|
||||||
|
validateStatus: (status) => true,
|
||||||
|
contentType: Headers.formUrlEncodedContentType
|
||||||
|
));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return Res(res.data!);
|
||||||
|
} else if(res.statusCode == 400) {
|
||||||
|
if(res.data.toString().contains("Access Token")) {
|
||||||
|
var refresh = await refreshToken();
|
||||||
|
if(refresh.success) {
|
||||||
|
return apiGet(path, query: query);
|
||||||
|
} else {
|
||||||
|
return Res.error(refresh.errorMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Res.error("Invalid Status Code: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
} else if((res.statusCode??500) < 500){
|
||||||
|
return Res.error(res.data?["error"]?["message"] ?? "Invalid Status code ${res.statusCode}");
|
||||||
|
} else {
|
||||||
|
return Res.error("Invalid Status Code: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get user details
|
||||||
|
Future<Res<UserDetails>> getUserDetails(Object userId) async{
|
||||||
|
var res = await apiGet("/v1/user/detail", query: {"user_id": userId, "filter": "for_android"});
|
||||||
|
if (res.success) {
|
||||||
|
return Res(UserDetails.fromJson(res.data));
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Illust>>> getRecommendedIllusts() async {
|
||||||
|
var res = await apiGet("/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true");
|
||||||
|
if (res.success) {
|
||||||
|
return Res((res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList());
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Illust>>> getBookmarkedIllusts(String restrict, [String? nextUrl]) async {
|
||||||
|
var res = await apiGet(nextUrl ?? "/v1/user/bookmarks/illust?user_id=49258688&restrict=$restrict");
|
||||||
|
if (res.success) {
|
||||||
|
return Res((res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), subData: res.data["next_url"]);
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> addBookmark(String id, String method, [String type = "public"]) async {
|
||||||
|
var res = method == "add" ? await apiPost("/v2/illust/bookmark/$method", data: {
|
||||||
|
"illust_id": id,
|
||||||
|
"restrict": type
|
||||||
|
}) : await apiPost("/v1/illust/bookmark/$method", data: {
|
||||||
|
"illust_id": id,
|
||||||
|
});
|
||||||
|
if(!res.error) {
|
||||||
|
return const Res(true);
|
||||||
|
} else {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> follow(String uid, String method, [String type = "public"]) async {
|
||||||
|
var res = method == "add" ? await apiPost("/v1/user/follow/add", data: {
|
||||||
|
"user_id": uid,
|
||||||
|
"restrict": type
|
||||||
|
}) : await apiPost("/v1/user/follow/delete", data: {
|
||||||
|
"user_id": uid,
|
||||||
|
});
|
||||||
|
if(!res.error) {
|
||||||
|
return const Res(true);
|
||||||
|
} else {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<TrendingTag>>> getHotTags() async {
|
||||||
|
var res = await apiGet("/v1/trending-tags/illust?filter=for_android&include_translated_tag_results=true");
|
||||||
|
if(res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
} else {
|
||||||
|
return Res(List.from(res.data["trend_tags"].map((e) => TrendingTag(
|
||||||
|
Tag(e["tag"], e["translated_name"]),
|
||||||
|
Illust.fromJson(e["illust"])
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
lib/network/res.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class Res<T>{
|
||||||
|
///error info
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
String get errorMessageWithoutNull => errorMessage??"Unknown Error";
|
||||||
|
|
||||||
|
/// data
|
||||||
|
final T? _data;
|
||||||
|
|
||||||
|
/// is there an error
|
||||||
|
bool get error => errorMessage!=null || _data==null;
|
||||||
|
|
||||||
|
/// whether succeed
|
||||||
|
bool get success => !error;
|
||||||
|
|
||||||
|
/// data
|
||||||
|
///
|
||||||
|
/// must be called when no error happened, or it will throw error
|
||||||
|
T get data => _data ?? (throw Exception(errorMessage));
|
||||||
|
|
||||||
|
/// get data, or null if there is an error
|
||||||
|
T? get dataOrNull => _data;
|
||||||
|
|
||||||
|
final dynamic subData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _data.toString();
|
||||||
|
|
||||||
|
Res.fromErrorRes(Res another, {this.subData}):
|
||||||
|
_data=null,errorMessage=another.errorMessageWithoutNull;
|
||||||
|
|
||||||
|
/// network result
|
||||||
|
const Res(this._data,{this.errorMessage, this.subData});
|
||||||
|
|
||||||
|
Res.error(dynamic e):errorMessage=e.toString(), _data=null, subData=null;
|
||||||
|
}
|
94
lib/pages/bookmarks.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:pixes/components/segmented_button.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
import '../components/illust_widget.dart';
|
||||||
|
import '../components/loading.dart';
|
||||||
|
|
||||||
|
class BookMarkedArtworkPage extends StatefulWidget {
|
||||||
|
const BookMarkedArtworkPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BookMarkedArtworkPage> createState() => _BookMarkedArtworkPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{
|
||||||
|
String restrict = "public";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
buildTab(),
|
||||||
|
Expanded(
|
||||||
|
child: _OneBookmarkedPage(restrict, key: Key(restrict),),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTab() {
|
||||||
|
return SegmentedButton(
|
||||||
|
options: [
|
||||||
|
SegmentedButtonOption("public", "Public".tl),
|
||||||
|
SegmentedButtonOption("private", "Private".tl),
|
||||||
|
],
|
||||||
|
onPressed: (key) {
|
||||||
|
if(key != restrict) {
|
||||||
|
setState(() {
|
||||||
|
restrict = key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: restrict,
|
||||||
|
).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneBookmarkedPage extends StatefulWidget {
|
||||||
|
const _OneBookmarkedPage(this.restrict, {super.key});
|
||||||
|
|
||||||
|
final String restrict;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OneBookmarkedPage> createState() => _OneBookmarkedPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneBookmarkedPageState extends MultiPageLoadingState<_OneBookmarkedPage, Illust> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, final List<Illust> data) {
|
||||||
|
return LayoutBuilder(builder: (context, constrains){
|
||||||
|
return MasonryGridView.builder(
|
||||||
|
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 240,
|
||||||
|
),
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if(index == data.length - 1){
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return IllustWidget(data[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Illust>>> loadData(page) async{
|
||||||
|
if(nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res = await Network().getBookmarkedIllusts(widget.restrict, nextUrl);
|
||||||
|
if(!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ?? "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
lib/pages/explore_page.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class ExplorePage extends StatelessWidget {
|
||||||
|
const ExplorePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Center(
|
||||||
|
child: Text("Explore"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
0
lib/pages/illust_detail_page.dart
Normal file
499
lib/pages/illust_page.dart
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/material.dart' show Icons;
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/download.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/image_page.dart';
|
||||||
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
import '../components/color_scheme.dart';
|
||||||
|
|
||||||
|
const _kBottomBarHeight = 64.0;
|
||||||
|
|
||||||
|
class IllustPage extends StatefulWidget {
|
||||||
|
const IllustPage(this.illust, {super.key});
|
||||||
|
|
||||||
|
final Illust illust;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<IllustPage> createState() => _IllustPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IllustPageState extends State<IllustPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: FluentTheme.of(context).micaBackgroundColor,
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: FluentTheme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
child: buildBody(constrains.maxWidth, constrains.maxHeight),
|
||||||
|
),
|
||||||
|
_BottomBar(widget.illust, constrains.maxHeight, constrains.maxWidth),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody(double width, double height) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: widget.illust.images.length + 2,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return buildImage(width, height, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildImage(double width, double height, int index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Text(
|
||||||
|
widget.illust.title,
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
).paddingVertical(8).paddingHorizontal(12);
|
||||||
|
}
|
||||||
|
index--;
|
||||||
|
if (index == widget.illust.images.length) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: _kBottomBarHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var imageWidth = width;
|
||||||
|
var imageHeight = widget.illust.height * width / widget.illust.width;
|
||||||
|
if (imageHeight > height) {
|
||||||
|
// 确保图片能够完整显示在屏幕上
|
||||||
|
var scale = imageHeight / height;
|
||||||
|
imageWidth = imageWidth / scale;
|
||||||
|
imageHeight = height;
|
||||||
|
}
|
||||||
|
var image = SizedBox(
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => ImagePage.show(widget.illust.images[index].original),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(widget.illust.images[index].medium),
|
||||||
|
width: imageWidth,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
height: imageHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
return Hero(
|
||||||
|
tag: "illust_${widget.illust.id}",
|
||||||
|
child: image,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomBar extends StatefulWidget {
|
||||||
|
const _BottomBar(this.illust, this.height, this.width);
|
||||||
|
|
||||||
|
final Illust illust;
|
||||||
|
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
final double width;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BottomBar> createState() => _BottomBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomBarState extends State<_BottomBar> {
|
||||||
|
double? top;
|
||||||
|
|
||||||
|
double pageHeight = 0;
|
||||||
|
|
||||||
|
double widgetHeight = 48;
|
||||||
|
|
||||||
|
final key = GlobalKey();
|
||||||
|
|
||||||
|
double _width = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_width = widget.width;
|
||||||
|
pageHeight = widget.height;
|
||||||
|
top = pageHeight - _kBottomBarHeight;
|
||||||
|
Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
|
final box = key.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
widgetHeight = (box?.size.height) ?? 0;
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _BottomBar oldWidget) {
|
||||||
|
if (widget.height != pageHeight) {
|
||||||
|
setState(() {
|
||||||
|
pageHeight = widget.height;
|
||||||
|
top = pageHeight - _kBottomBarHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(_width != widget.width) {
|
||||||
|
_width = widget.width;
|
||||||
|
Future.microtask(() {
|
||||||
|
final box = key.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
var oldHeight = widgetHeight;
|
||||||
|
widgetHeight = (box?.size.height) ?? 0;
|
||||||
|
if(oldHeight != widgetHeight && top != pageHeight - _kBottomBarHeight) {
|
||||||
|
setState(() {
|
||||||
|
top = pageHeight - widgetHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedPositioned(
|
||||||
|
top: top,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.ease,
|
||||||
|
child: Card(
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||||
|
backgroundColor:
|
||||||
|
FluentTheme.of(context).micaBackgroundColor.withOpacity(0.96),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
key: key,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildTop(),
|
||||||
|
buildStats(),
|
||||||
|
buildTags(),
|
||||||
|
SelectableText("${"Artwork ID".tl}: ${widget.illust.id}\n${"Artist ID".tl}: ${widget.illust.author.id}", style: TextStyle(color: ColorScheme.of(context).outline),).paddingLeft(4),
|
||||||
|
const SizedBox(height: 8,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTop() {
|
||||||
|
return SizedBox(
|
||||||
|
height: _kBottomBarHeight,
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
buildAuthor(),
|
||||||
|
...buildActions(constrains.maxWidth),
|
||||||
|
const Spacer(),
|
||||||
|
if (top == pageHeight - _kBottomBarHeight)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(FluentIcons.up),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
top = pageHeight - widgetHeight;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(FluentIcons.down),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
top = pageHeight - _kBottomBarHeight;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFollowing = false;
|
||||||
|
|
||||||
|
Widget buildAuthor() {
|
||||||
|
void follow() async{
|
||||||
|
if(isFollowing) return;
|
||||||
|
setState(() {
|
||||||
|
isFollowing = true;
|
||||||
|
});
|
||||||
|
var method = widget.illust.author.isFollowed ? "delete" : "add";
|
||||||
|
var res = await Network().follow(widget.illust.author.id.toString(), method);
|
||||||
|
if(res.error) {
|
||||||
|
if(mounted) {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widget.illust.author.isFollowed = !widget.illust.author.isFollowed;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isFollowing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
backgroundColor: FluentTheme.of(context).cardColor.withOpacity(0.72),
|
||||||
|
child: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
width: 246,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => context.to(() =>
|
||||||
|
UserInfoPage(widget.illust.author.id.toString())),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(widget.illust.author.avatar),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.illust.author.name,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if(isFollowing)
|
||||||
|
Button(onPressed: follow, child: const SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 24,
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 18,
|
||||||
|
child: ProgressRing(strokeWidth: 2,),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
else if (!widget.illust.author.isFollowed)
|
||||||
|
Button(onPressed: follow, child: Text("Follow".tl))
|
||||||
|
else
|
||||||
|
Button(
|
||||||
|
onPressed: follow,
|
||||||
|
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).errorColor),),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isBookmarking = false;
|
||||||
|
|
||||||
|
Iterable<Widget> buildActions(double width) sync* {
|
||||||
|
yield const SizedBox(width: 8,);
|
||||||
|
|
||||||
|
void favorite() async{
|
||||||
|
if(isBookmarking) return;
|
||||||
|
setState(() {
|
||||||
|
isBookmarking = true;
|
||||||
|
});
|
||||||
|
var method = widget.illust.isBookmarked ? "delete" : "add";
|
||||||
|
var res = await Network().addBookmark(widget.illust.id.toString(), method);
|
||||||
|
if(res.error) {
|
||||||
|
if(mounted) {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widget.illust.isBookmarked = !widget.illust.isBookmarked;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isBookmarking = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void download() {}
|
||||||
|
|
||||||
|
bool showText = width > 640;
|
||||||
|
|
||||||
|
yield Button(
|
||||||
|
onPressed: favorite,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if(isBookmarking)
|
||||||
|
const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: ProgressRing(strokeWidth: 2,),
|
||||||
|
)
|
||||||
|
else if(widget.illust.isBookmarked)
|
||||||
|
Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
color: ColorScheme.of(context).errorColor,
|
||||||
|
size: 18,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Icon(
|
||||||
|
Icons.favorite_border,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
if(showText)
|
||||||
|
const SizedBox(width: 8,),
|
||||||
|
if(showText)
|
||||||
|
if(widget.illust.isBookmarked)
|
||||||
|
Text("Cancel".tl)
|
||||||
|
else
|
||||||
|
Text("Favorite".tl)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
yield const SizedBox(width: 8,);
|
||||||
|
|
||||||
|
if (!widget.illust.downloaded) {
|
||||||
|
yield Button(
|
||||||
|
onPressed: download,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
FluentIcons.download,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
if(showText)
|
||||||
|
const SizedBox(width: 8,),
|
||||||
|
if(showText)
|
||||||
|
Text("Download".tl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield const SizedBox(width: 8,);
|
||||||
|
|
||||||
|
yield Button(
|
||||||
|
onPressed: favorite,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
FluentIcons.comment,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
if(showText)
|
||||||
|
const SizedBox(width: 8,),
|
||||||
|
if(showText)
|
||||||
|
Text("Comment".tl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildStats(){
|
||||||
|
return SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 2,),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(4)
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(FluentIcons.view, size: 20,),
|
||||||
|
Text("Views".tl, style: const TextStyle(fontSize: 12),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12,),
|
||||||
|
Text(widget.illust.totalView.toString(), style: TextStyle(color: ColorScheme.of(context).primary, fontWeight: FontWeight.w500, fontSize: 18),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16,),
|
||||||
|
Expanded(child: Container(
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(4)
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(FluentIcons.six_point_star, size: 20,),
|
||||||
|
Text("Favorites".tl, style: const TextStyle(fontSize: 12),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12,),
|
||||||
|
Text(widget.illust.totalBookmarks.toString(), style: TextStyle(color: ColorScheme.of(context).primary, fontWeight: FontWeight.w500, fontSize: 18),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const SizedBox(width: 2,),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTags() {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: widget.illust.tags.map((e) {
|
||||||
|
var text = e.name;
|
||||||
|
if(e.translatedName != null && e.name != e.translatedName) {
|
||||||
|
text += "/${e.translatedName}";
|
||||||
|
}
|
||||||
|
return Card(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
||||||
|
child: Text(text, style: const TextStyle(fontSize: 13),),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
).paddingVertical(8).paddingHorizontal(2);
|
||||||
|
}
|
||||||
|
}
|
90
lib/pages/image_page.dart
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:pixes/components/page_route.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/pages/main_page.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
class ImagePage extends StatefulWidget {
|
||||||
|
const ImagePage(this.url, {super.key});
|
||||||
|
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
static show(String url) {
|
||||||
|
App.rootNavigatorKey.currentState?.push(
|
||||||
|
AppPageRoute(builder: (context) => ImagePage(url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImagePage> createState() => _ImagePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImagePageState extends State<ImagePage> with WindowListener{
|
||||||
|
int windowButtonKey = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
windowManager.addListener(this);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMaximize() {
|
||||||
|
setState(() {
|
||||||
|
windowButtonKey++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowUnmaximize() {
|
||||||
|
setState(() {
|
||||||
|
windowButtonKey++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(1),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: PhotoView(
|
||||||
|
backgroundDecoration: const BoxDecoration(
|
||||||
|
color: Colors.transparent
|
||||||
|
),
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
imageProvider: CachedImageProvider(widget.url),
|
||||||
|
)),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 6,),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(FluentIcons.back).paddingAll(2),
|
||||||
|
onPressed: () => context.pop()
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: DragToMoveArea(child: SizedBox.expand(),),
|
||||||
|
),
|
||||||
|
WindowButtons(key: ValueKey(windowButtonKey),),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
218
lib/pages/login_page.dart
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/utils/app_links.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class LoginPage extends StatefulWidget {
|
||||||
|
const LoginPage(this.callback, {super.key});
|
||||||
|
|
||||||
|
final void Function() callback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginPage> createState() => _LoginPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPageState extends State<LoginPage> {
|
||||||
|
bool checked = false;
|
||||||
|
|
||||||
|
bool waitingForAuth = false;
|
||||||
|
|
||||||
|
bool isLogging = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!waitingForAuth) {
|
||||||
|
return buildLogin(context);
|
||||||
|
} else {
|
||||||
|
return buildWaiting(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLogin(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
child: Center(
|
||||||
|
child: Card(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
"Login".tl,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (checked)
|
||||||
|
FilledButton(
|
||||||
|
onPressed: onContinue,
|
||||||
|
child: Text("Continue".tl),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
height: 28,
|
||||||
|
width: 78,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context)
|
||||||
|
.inactiveBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
child: Center(
|
||||||
|
child: Text("Continue".tl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 4),
|
||||||
|
child: Text(
|
||||||
|
"You need to complete the login operation in the browser window that will open."
|
||||||
|
.tl),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
checked: checked,
|
||||||
|
onChanged: (value) => setState(() {
|
||||||
|
checked = value ?? false;
|
||||||
|
})),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Text("I have read and agree to the Terms of Use".tl)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildWaiting(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
child: Center(
|
||||||
|
child: Card(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
"Waiting...".tl,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 4),
|
||||||
|
child: Text(
|
||||||
|
"Waiting for authentication. Please finished in the browser."
|
||||||
|
.tl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Button(
|
||||||
|
child: Text("Back".tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
waitingForAuth = false;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLoading(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
child: Center(
|
||||||
|
child: Card(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
"Logging in".tl,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: ProgressRing(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onContinue() async {
|
||||||
|
var url = await Network().generateWebviewUrl();
|
||||||
|
launchUrlString(url);
|
||||||
|
onLink = (uri) {
|
||||||
|
if (uri.scheme == "pixiv") {
|
||||||
|
onFinished(uri.queryParameters["code"]!);
|
||||||
|
onLink = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
setState(() {
|
||||||
|
waitingForAuth = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFinished(String code) async {
|
||||||
|
setState(() {
|
||||||
|
isLogging = true;
|
||||||
|
waitingForAuth = false;
|
||||||
|
});
|
||||||
|
var res = await Network().loginWithCode(code);
|
||||||
|
if (res.error) {
|
||||||
|
if(mounted) {
|
||||||
|
context.showToast(message: res.errorMessage!);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLogging = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
widget.callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
619
lib/pages/main_page.dart
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
import "dart:async";
|
||||||
|
|
||||||
|
import "package:fluent_ui/fluent_ui.dart";
|
||||||
|
import "package:pixes/appdata.dart";
|
||||||
|
import "package:pixes/components/color_scheme.dart";
|
||||||
|
import "package:pixes/components/md.dart";
|
||||||
|
import "package:pixes/foundation/app.dart";
|
||||||
|
import "package:pixes/network/network.dart";
|
||||||
|
import "package:pixes/pages/bookmarks.dart";
|
||||||
|
import "package:pixes/pages/explore_page.dart";
|
||||||
|
import "package:pixes/pages/recommendation_page.dart";
|
||||||
|
import "package:pixes/pages/login_page.dart";
|
||||||
|
import "package:pixes/pages/search_page.dart";
|
||||||
|
import "package:pixes/pages/settings_page.dart";
|
||||||
|
import "package:pixes/pages/user_info_page.dart";
|
||||||
|
import "package:pixes/utils/mouse_listener.dart";
|
||||||
|
import "package:pixes/utils/translation.dart";
|
||||||
|
import "package:window_manager/window_manager.dart";
|
||||||
|
|
||||||
|
import "../components/page_route.dart";
|
||||||
|
|
||||||
|
const _kAppBarHeight = 36.0;
|
||||||
|
|
||||||
|
class MainPage extends StatefulWidget {
|
||||||
|
const MainPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MainPage> createState() => _MainPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainPageState extends State<MainPage> with WindowListener {
|
||||||
|
final navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
int index = 1;
|
||||||
|
|
||||||
|
int windowButtonKey = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
windowManager.addListener(this);
|
||||||
|
listenMouseSideButtonToBack(navigatorKey);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMaximize() {
|
||||||
|
setState(() {
|
||||||
|
windowButtonKey++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowUnmaximize() {
|
||||||
|
setState(() {
|
||||||
|
windowButtonKey++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isLogin => Network().token != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!isLogin) {
|
||||||
|
return NavigationView(
|
||||||
|
appBar: buildAppBar(context, navigatorKey),
|
||||||
|
content: LoginPage(() => setState(() {})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ColorScheme(
|
||||||
|
brightness: FluentTheme.of(context).brightness,
|
||||||
|
child: NavigationView(
|
||||||
|
appBar: buildAppBar(context, navigatorKey),
|
||||||
|
pane: NavigationPane(
|
||||||
|
selected: index,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
index = value;
|
||||||
|
});
|
||||||
|
navigate(value);
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
UserPane(),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.search, size: 20,),
|
||||||
|
title: Text('Search'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItemHeader(header: Text("Artwork".tl).paddingVertical(4).paddingLeft(8)),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.star_border, size: 20,),
|
||||||
|
title: Text('Recommendations'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.bookmark_outline, size: 20),
|
||||||
|
title: Text('Bookmarks'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItemSeparator(),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.explore_outlined, size: 20),
|
||||||
|
title: Text('Explore'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
footerItems: [
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.settings_outlined, size: 20),
|
||||||
|
title: Text('Settings'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
paneBodyBuilder: (pane, child) => Navigator(
|
||||||
|
key: navigatorKey,
|
||||||
|
onGenerateRoute: (settings) => AppPageRoute(
|
||||||
|
builder: (context) => const RecommendationPage()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static final pageBuilders = [
|
||||||
|
() => UserInfoPage(appdata.account!.user.id),
|
||||||
|
() => const SearchPage(),
|
||||||
|
() => const RecommendationPage(),
|
||||||
|
() => const BookMarkedArtworkPage(),
|
||||||
|
() => const ExplorePage(),
|
||||||
|
() => const SettingsPage(),
|
||||||
|
];
|
||||||
|
|
||||||
|
void navigate(int index) {
|
||||||
|
var page = pageBuilders.elementAtOrNull(index) ??
|
||||||
|
() => Center(
|
||||||
|
child: Text("Invalid Page: $index"),
|
||||||
|
);
|
||||||
|
navigatorKey.currentState!.pushAndRemoveUntil(
|
||||||
|
AppPageRoute(builder: (context) => page()), (route) => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationAppBar buildAppBar(
|
||||||
|
BuildContext context, GlobalKey<NavigatorState> navigatorKey) {
|
||||||
|
return NavigationAppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
height: _kAppBarHeight,
|
||||||
|
title: () {
|
||||||
|
if (!App.isDesktop) {
|
||||||
|
return const Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Text("pixes"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const DragToMoveArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 4),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Text(
|
||||||
|
"Pixes",
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
leading: _BackButton(navigatorKey),
|
||||||
|
actions: WindowButtons(
|
||||||
|
key: ValueKey(windowButtonKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackButton extends StatefulWidget {
|
||||||
|
const _BackButton(this.navigatorKey);
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BackButton> createState() => _BackButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackButtonState extends State<_BackButton> {
|
||||||
|
GlobalKey<NavigatorState> get navigatorKey => widget.navigatorKey;
|
||||||
|
|
||||||
|
bool enabled = false;
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
enabled = navigatorKey.currentState?.canPop() == true;
|
||||||
|
loop();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||||
|
if(!mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
} else {
|
||||||
|
bool enabled = navigatorKey.currentState?.canPop() == true;
|
||||||
|
if(enabled != this.enabled) {
|
||||||
|
setState(() {
|
||||||
|
this.enabled = enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
void onPressed() {
|
||||||
|
if (navigatorKey.currentState?.canPop() ?? false) {
|
||||||
|
navigatorKey.currentState?.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NavigationPaneTheme(
|
||||||
|
data: NavigationPaneTheme.of(context).merge(NavigationPaneThemeData(
|
||||||
|
unselectedIconColor: ButtonState.resolveWith((states) {
|
||||||
|
if (states.isDisabled) {
|
||||||
|
return ButtonThemeData.buttonColor(context, states);
|
||||||
|
}
|
||||||
|
return ButtonThemeData.uncheckedInputColor(
|
||||||
|
FluentTheme.of(context),
|
||||||
|
states,
|
||||||
|
).basedOnLuminance();
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => PaneItem(
|
||||||
|
icon: const Center(child: Icon(FluentIcons.back, size: 12.0)),
|
||||||
|
title: const Text("Back"),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
enabled: enabled,
|
||||||
|
).build(
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
onPressed,
|
||||||
|
displayMode: PaneDisplayMode.compact,
|
||||||
|
).paddingTop(2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WindowButtons extends StatelessWidget {
|
||||||
|
const WindowButtons({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final FluentThemeData theme = FluentTheme.of(context);
|
||||||
|
final color = theme.iconTheme.color ?? Colors.black;
|
||||||
|
final hoverColor = theme.inactiveBackgroundColor;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: 138,
|
||||||
|
height: _kAppBarHeight,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
WindowButton(
|
||||||
|
icon: MinimizeIcon(color: color),
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
onPressed: () async {
|
||||||
|
bool isMinimized = await windowManager.isMinimized();
|
||||||
|
if (isMinimized) {
|
||||||
|
windowManager.restore();
|
||||||
|
} else {
|
||||||
|
windowManager.minimize();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FutureBuilder<bool>(
|
||||||
|
future: windowManager.isMaximized(),
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||||
|
if (snapshot.data == true) {
|
||||||
|
return WindowButton(
|
||||||
|
icon: RestoreIcon(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
onPressed: () {
|
||||||
|
windowManager.unmaximize();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return WindowButton(
|
||||||
|
icon: MaximizeIcon(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
onPressed: () {
|
||||||
|
windowManager.maximize();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
WindowButton(
|
||||||
|
icon: CloseIcon(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
hoverIcon: CloseIcon(
|
||||||
|
color: theme.brightness == Brightness.light
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
),
|
||||||
|
hoverColor: Colors.red,
|
||||||
|
onPressed: () {
|
||||||
|
windowManager.close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowButton extends StatefulWidget {
|
||||||
|
const WindowButton(
|
||||||
|
{required this.icon,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.hoverColor,
|
||||||
|
this.hoverIcon,
|
||||||
|
super.key});
|
||||||
|
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
|
final void Function() onPressed;
|
||||||
|
|
||||||
|
final Color hoverColor;
|
||||||
|
|
||||||
|
final Widget? hoverIcon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WindowButton> createState() => _WindowButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WindowButtonState extends State<WindowButton> {
|
||||||
|
bool isHovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (event) => setState(() {
|
||||||
|
isHovering = true;
|
||||||
|
}),
|
||||||
|
onExit: (event) => setState(() {
|
||||||
|
isHovering = false;
|
||||||
|
}),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
child: Container(
|
||||||
|
width: 46,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(color: isHovering ? widget.hoverColor : null),
|
||||||
|
child: isHovering ? widget.hoverIcon ?? widget.icon : widget.icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserPane extends PaneItem {
|
||||||
|
UserPane() : super(icon: const SizedBox(), body: const SizedBox());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, bool selected, VoidCallback? onPressed,
|
||||||
|
{PaneDisplayMode? displayMode,
|
||||||
|
bool showTextOnTop = true,
|
||||||
|
int? itemIndex,
|
||||||
|
bool? autofocus}) {
|
||||||
|
final maybeBody = NavigationView.maybeOf(context);
|
||||||
|
var mode = displayMode ?? maybeBody?.displayMode ?? PaneDisplayMode.minimal;
|
||||||
|
|
||||||
|
if (maybeBody?.compactOverlayOpen == true) {
|
||||||
|
mode = PaneDisplayMode.open;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget body = () {
|
||||||
|
switch (mode) {
|
||||||
|
case PaneDisplayMode.minimal:
|
||||||
|
case PaneDisplayMode.open:
|
||||||
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
|
if (constrains.maxHeight < 72 || constrains.maxWidth < 120) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 64,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(48),
|
||||||
|
child: Image(
|
||||||
|
height: 48,
|
||||||
|
width: 48,
|
||||||
|
image: NetworkImage(appdata.account!.user.profile),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
if (constrains.maxWidth > 90)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
appdata.account!.user.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
appdata.account!.user.email,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
case PaneDisplayMode.compact:
|
||||||
|
case PaneDisplayMode.top:
|
||||||
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
|
if (constrains.maxHeight < 48 || constrains.maxWidth < 32) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
child: Image(
|
||||||
|
height: 30,
|
||||||
|
width: 30,
|
||||||
|
image: NetworkImage(appdata.account!.user.profile),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
).paddingAll(4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw "Invalid Display mode";
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
var button = HoverButton(
|
||||||
|
builder: (context, states) {
|
||||||
|
final theme = NavigationPaneTheme.of(context);
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: () {
|
||||||
|
final tileColor = this.tileColor ??
|
||||||
|
theme.tileColor ??
|
||||||
|
kDefaultPaneItemColor(context, mode == PaneDisplayMode.top);
|
||||||
|
final newStates = states.toSet()..remove(ButtonStates.disabled);
|
||||||
|
if (selected && selectedTileColor != null) {
|
||||||
|
return selectedTileColor!.resolve(newStates);
|
||||||
|
}
|
||||||
|
return tileColor.resolve(
|
||||||
|
selected
|
||||||
|
? {
|
||||||
|
states.isHovering
|
||||||
|
? ButtonStates.pressing
|
||||||
|
: ButtonStates.hovering,
|
||||||
|
}
|
||||||
|
: newStates,
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: FocusBorder(
|
||||||
|
focused: states.isFocused,
|
||||||
|
renderOutside: false,
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
key: key,
|
||||||
|
padding: const EdgeInsetsDirectional.only(bottom: 4.0),
|
||||||
|
child: button,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close
|
||||||
|
class CloseIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
const CloseIcon({super.key, required this.color});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_ClosePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClosePainter extends _IconPainter {
|
||||||
|
_ClosePainter(super.color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color, true);
|
||||||
|
canvas.drawLine(const Offset(0, 0), Offset(size.width, size.height), p);
|
||||||
|
canvas.drawLine(Offset(0, size.height), Offset(size.width, 0), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximize
|
||||||
|
class MaximizeIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
const MaximizeIcon({super.key, required this.color});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MaximizePainter extends _IconPainter {
|
||||||
|
_MaximizePainter(super.color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color);
|
||||||
|
canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore
|
||||||
|
class RestoreIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
const RestoreIcon({
|
||||||
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RestorePainter extends _IconPainter {
|
||||||
|
_RestorePainter(super.color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color);
|
||||||
|
canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
|
||||||
|
canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
|
||||||
|
canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(size.width, 0), Offset(size.width, size.height - 2), p);
|
||||||
|
canvas.drawLine(Offset(size.width, size.height - 2),
|
||||||
|
Offset(size.width - 2, size.height - 2), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimize
|
||||||
|
class MinimizeIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
const MinimizeIcon({super.key, required this.color});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MinimizePainter extends _IconPainter {
|
||||||
|
_MinimizePainter(super.color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helpers
|
||||||
|
abstract class _IconPainter extends CustomPainter {
|
||||||
|
_IconPainter(this.color);
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlignedPaint extends StatelessWidget {
|
||||||
|
const _AlignedPaint(this.painter);
|
||||||
|
final CustomPainter painter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: CustomPaint(size: const Size(10, 10), painter: painter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
|
||||||
|
..color = color
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..isAntiAlias = isAntiAlias
|
||||||
|
..strokeWidth = 1;
|
15
lib/pages/ranking.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
|
class RankingPage extends StatefulWidget {
|
||||||
|
const RankingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RankingPage> createState() => _RankingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RankingPageState extends State<RankingPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
38
lib/pages/recommendation_page.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:pixes/components/illust_widget.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/network/res.dart';
|
||||||
|
|
||||||
|
class RecommendationPage extends StatefulWidget {
|
||||||
|
const RecommendationPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecommendationPage> createState() => _RecommendationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecommendationPageState extends MultiPageLoadingState<RecommendationPage, Illust> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, final List<Illust> data) {
|
||||||
|
return LayoutBuilder(builder: (context, constrains){
|
||||||
|
return MasonryGridView.builder(
|
||||||
|
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 240,
|
||||||
|
),
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if(index == data.length - 1){
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return IllustWidget(data[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Illust>>> loadData(page) {
|
||||||
|
return Network().getRecommendedIllusts();
|
||||||
|
}
|
||||||
|
}
|
238
lib/pages/search_page.dart
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
import '../components/animated_image.dart';
|
||||||
|
import '../components/color_scheme.dart';
|
||||||
|
import '../foundation/image_provider.dart';
|
||||||
|
|
||||||
|
class SearchPage extends StatefulWidget {
|
||||||
|
const SearchPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchPage> createState() => _SearchPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPageState extends State<SearchPage> {
|
||||||
|
String text = "";
|
||||||
|
|
||||||
|
int searchType = 0;
|
||||||
|
|
||||||
|
void search() {
|
||||||
|
switch(searchType) {
|
||||||
|
case 0:
|
||||||
|
context.to(() => SearchResultPage(text));
|
||||||
|
case 1:
|
||||||
|
// TODO: artwork by id
|
||||||
|
throw UnimplementedError();
|
||||||
|
case 2:
|
||||||
|
context.to(() => UserInfoPage(text));
|
||||||
|
case 3:
|
||||||
|
// TODO: novel page
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ScaffoldPage(
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
buildSearchBar(),
|
||||||
|
const SizedBox(height: 8,),
|
||||||
|
const Expanded(
|
||||||
|
child: _TrendingTagsView(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final optionController = FlyoutController();
|
||||||
|
|
||||||
|
Widget buildSearchBar() {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 42,
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constrains) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 42,
|
||||||
|
width: constrains.maxWidth,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextBox(
|
||||||
|
placeholder: searchTypes[searchType].tl,
|
||||||
|
onChanged: (s) => text = s,
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context)
|
||||||
|
.outlineVariant
|
||||||
|
.withOpacity(0.6)),
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
suffix: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: search,
|
||||||
|
child: const Icon(
|
||||||
|
FluentIcons.search,
|
||||||
|
size: 16,
|
||||||
|
).paddingHorizontal(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
FlyoutTarget(
|
||||||
|
controller: optionController,
|
||||||
|
child: Button(
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 42,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(FluentIcons.chevron_down),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
optionController.showFlyout(
|
||||||
|
navigatorKey: App.rootNavigatorKey.currentState,
|
||||||
|
builder: buildSearchOption,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const searchTypes = [
|
||||||
|
"Keyword search",
|
||||||
|
"Artwork ID",
|
||||||
|
"Artist ID",
|
||||||
|
"Novel ID"
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget buildSearchOption(BuildContext context) {
|
||||||
|
return MenuFlyout(
|
||||||
|
items: List.generate(
|
||||||
|
searchTypes.length,
|
||||||
|
(index) => MenuFlyoutItem(
|
||||||
|
text: Text(searchTypes[index].tl),
|
||||||
|
onPressed: () => setState(() => searchType = index))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrendingTagsView extends StatefulWidget {
|
||||||
|
const _TrendingTagsView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TrendingTagsView> createState() => _TrendingTagsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List<TrendingTag>> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<TrendingTag> data) {
|
||||||
|
return MasonryGridView.builder(
|
||||||
|
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 240,
|
||||||
|
),
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return buildItem(data[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItem(TrendingTag tag) {
|
||||||
|
final illust = tag.illust;
|
||||||
|
|
||||||
|
var text = tag.tag.name;
|
||||||
|
if(tag.tag.translatedName != null) {
|
||||||
|
text += "/${tag.tag.translatedName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
|
final width = constrains.maxWidth;
|
||||||
|
final height = illust.height * width / illust.width;
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||||
|
child: Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: (){
|
||||||
|
context.to(() => SearchResultPage(tag.tag.name));
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(illust.images.first.medium),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: width-16.0,
|
||||||
|
height: height-16.0,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Positioned(
|
||||||
|
bottom: -2,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.84),
|
||||||
|
borderRadius: BorderRadius.circular(4)
|
||||||
|
),
|
||||||
|
child: Text(text).paddingHorizontal(4).paddingVertical(6).paddingBottom(2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<TrendingTag>>> loadData() {
|
||||||
|
return Network().getHotTags();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SearchResultPage extends StatefulWidget {
|
||||||
|
const SearchResultPage(this.keyword, {super.key});
|
||||||
|
|
||||||
|
final String keyword;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchResultPage> createState() => _SearchResultPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchResultPageState extends State<SearchResultPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const ScaffoldPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
lib/pages/settings_page.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
|
class SettingsPage extends StatefulWidget {
|
||||||
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
54
lib/pages/user_info_page.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/network/res.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
class UserInfoPage extends StatefulWidget {
|
||||||
|
const UserInfoPage(this.id, {super.key});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UserInfoPage> createState() => _UserInfoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, UserDetails data) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(64),
|
||||||
|
child: Image(
|
||||||
|
image: CachedImageProvider(data.avatar),
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(data.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: 'Follows: '.tl),
|
||||||
|
TextSpan(text: '${data.totalFollowUsers}', style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<UserDetails>> loadData() {
|
||||||
|
return Network().getUserDetails(widget.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
lib/utils/app_links.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:app_links/app_links.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/log.dart';
|
||||||
|
import 'package:win32_registry/win32_registry.dart';
|
||||||
|
|
||||||
|
Future<void> _register(String scheme) async {
|
||||||
|
String appPath = Platform.resolvedExecutable;
|
||||||
|
|
||||||
|
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||||
|
RegistryValue protocolRegValue = const RegistryValue(
|
||||||
|
'URL Protocol',
|
||||||
|
RegistryValueType.string,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
String protocolCmdRegKey = 'shell\\open\\command';
|
||||||
|
RegistryValue protocolCmdRegValue = RegistryValue(
|
||||||
|
'',
|
||||||
|
RegistryValueType.string,
|
||||||
|
'"$appPath" "%1"',
|
||||||
|
);
|
||||||
|
|
||||||
|
final regKey = Registry.currentUser.createKey(protocolRegKey);
|
||||||
|
regKey.createValue(protocolRegValue);
|
||||||
|
regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Function(Uri uri)? onLink;
|
||||||
|
|
||||||
|
void handleLinks() async {
|
||||||
|
if (App.isWindows) {
|
||||||
|
await _register("pixiv");
|
||||||
|
}
|
||||||
|
AppLinks().uriLinkStream.listen((uri) {
|
||||||
|
Log.info("App Link", uri.toString());
|
||||||
|
if (onLink?.call(uri) == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
86
lib/utils/ext.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
extension ListExt<T> on List<T>{
|
||||||
|
/// Remove all blank value and return the list.
|
||||||
|
List<T> getNoBlankList(){
|
||||||
|
List<T> newList = [];
|
||||||
|
for(var value in this){
|
||||||
|
if(value.toString() != ""){
|
||||||
|
newList.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
|
|
||||||
|
T? firstWhereOrNull(bool Function(T element) test){
|
||||||
|
for(var element in this){
|
||||||
|
if(test(element)){
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addIfNotNull(T? value){
|
||||||
|
if(value != null){
|
||||||
|
add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StringExt on String{
|
||||||
|
///Remove all value that would display blank on the screen.
|
||||||
|
String get removeAllBlank => replaceAll("\n", "").replaceAll(" ", "").replaceAll("\t", "");
|
||||||
|
|
||||||
|
/// convert this to a one-element list.
|
||||||
|
List<String> toList() => [this];
|
||||||
|
|
||||||
|
String _nums(){
|
||||||
|
String res = "";
|
||||||
|
for(int i=0; i<length; i++){
|
||||||
|
res += this[i].isNum?this[i]:"";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get nums => _nums();
|
||||||
|
|
||||||
|
String setValueAt(String value, int index){
|
||||||
|
return replaceRange(index, index+1, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? subStringOrNull(int start, [int? end]){
|
||||||
|
if(start < 0 || (end != null && end > length)){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return substring(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
String replaceLast(String from, String to) {
|
||||||
|
if (isEmpty || from.isEmpty) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastIndex = lastIndexOf(from);
|
||||||
|
if (lastIndex == -1) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final before = substring(0, lastIndex);
|
||||||
|
final after = substring(lastIndex + from.length);
|
||||||
|
return '$before$to$after';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasMatch(String? value, String pattern) {
|
||||||
|
return (value == null) ? false : RegExp(pattern).hasMatch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isURL(){
|
||||||
|
final regex = RegExp(
|
||||||
|
r'^((http|https|ftp)://)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$',
|
||||||
|
caseSensitive: false);
|
||||||
|
return regex.hasMatch(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isURL => _isURL();
|
||||||
|
|
||||||
|
bool get isNum => double.tryParse(this) != null;
|
||||||
|
}
|
22
lib/utils/io.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
extension FSExt on FileSystemEntity {
|
||||||
|
Future<void> deleteIfExists() async {
|
||||||
|
if (await exists()) {
|
||||||
|
await delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get size {
|
||||||
|
if (this is File) {
|
||||||
|
return (this as File).lengthSync();
|
||||||
|
} else if(this is Directory){
|
||||||
|
var size = 0;
|
||||||
|
for(var file in (this as Directory).listSync()){
|
||||||
|
size += file.size;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|