commit b095643cbc66202c5b47697b8daaa3e7d61b1e22 Author: wgh19 Date: Mon May 13 09:36:23 2024 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..c153808 --- /dev/null +++ b/.metadata @@ -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' diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d12925 --- /dev/null +++ b/README.md @@ -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. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -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 diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -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 diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..3a4795c --- /dev/null +++ b/android/app/build.gradle @@ -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 {} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d7fa97 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/github/wgh136/pixes/MainActivity.kt b/android/app/src/main/kotlin/com/github/wgh136/pixes/MainActivity.kt new file mode 100644 index 0000000..b40c8d8 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/wgh136/pixes/MainActivity.kt @@ -0,0 +1,5 @@ +package com.github.wgh136.pixes + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..bc157bd --- /dev/null +++ b/android/build.gradle @@ -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 +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e1ca574 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..1d6d19b --- /dev/null +++ b/android/settings.gradle @@ -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" diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -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 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a356e08 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -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) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..ef53bb5 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Pixes + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + pixes + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/lib/appdata.dart b/lib/appdata.dart new file mode 100644 index 0000000..87019de --- /dev/null +++ b/lib/appdata.dart @@ -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 readData() async { + final file = File("${App.dataPath}/account.json"); + if (file.existsSync()) { + account = Account.fromJson(jsonDecode(await file.readAsString())); + } + } +} + +final appdata = _Appdata(); diff --git a/lib/components/animated_image.dart b/lib/components/animated_image.dart new file mode 100644 index 0000000..ea221c4 --- /dev/null +++ b/lib/components/animated_image.dart @@ -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? 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? opacity; + + final Color? color; + + final bool isAntiAlias; + + static void clear() => _AnimatedImageState.clear(); + + @override + State createState() => _AnimatedImageState(); +} + +class _AnimatedImageState extends State with WidgetsBindingObserver { + ImageStream? _imageStream; + ImageInfo? _imageInfo; + ImageChunkEvent? _loadingProgress; + bool _isListeningToStream = false; + late bool _invertColors; + int? _frameNumber; + bool _wasSynchronouslyLoaded = false; + late DisposableBuildContext> _scrollAwareContext; + Object? _lastException; + ImageStreamCompleterHandle? _completerHandle; + + static final Map _cache = {}; + + static clear() => _cache.clear(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _scrollAwareContext = DisposableBuildContext>(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( + 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('stream', _imageStream)); + description.add(DiagnosticsProperty('pixels', _imageInfo)); + description.add(DiagnosticsProperty('loadingProgress', _loadingProgress)); + description.add(DiagnosticsProperty('frameNumber', _frameNumber)); + description.add(DiagnosticsProperty('wasSynchronouslyLoaded', _wasSynchronouslyLoaded)); + } +} diff --git a/lib/components/color_scheme.dart b/lib/components/color_scheme.dart new file mode 100644 index 0000000..d2a827d --- /dev/null +++ b/lib/components/color_scheme.dart @@ -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()!; + } + + 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; + } +} \ No newline at end of file diff --git a/lib/components/illust_widget.dart b/lib/components/illust_widget.dart new file mode 100644 index 0000000..89980a3 --- /dev/null +++ b/lib/components/illust_widget.dart @@ -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, + ), + ), + ), + ), + ), + ); + }); + } +} diff --git a/lib/components/loading.dart b/lib/components/loading.dart new file mode 100644 index 0000000..e9de33b --- /dev/null +++ b/lib/components/loading.dart @@ -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 extends State{ + bool isLoading = true; + + S? data; + + String? error; + + Future> 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 extends State{ + bool _isFirstLoading = true; + + bool _isLoading = false; + + List? _data; + + String? _error; + + int _page = 1; + + Future>> loadData(int page); + + Widget buildContent(BuildContext context, final List 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!); + } + } +} diff --git a/lib/components/md.dart b/lib/components/md.dart new file mode 100644 index 0000000..2c5b65f --- /dev/null +++ b/lib/components/md.dart @@ -0,0 +1,3 @@ +import 'package:flutter/material.dart'; + +typedef MdIcons = Icons; \ No newline at end of file diff --git a/lib/components/message.dart b/lib/components/message.dart new file mode 100644 index 0000000..1166ca5 --- /dev/null +++ b/lib/components/message.dart @@ -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(context); + } + + @override + State createState() => OverlayWidgetState(); +} + +class OverlayWidgetState extends State { + var overlayKey = GlobalKey(); + + var entries = []; + + 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)], + ); + } +} diff --git a/lib/components/page_route.dart b/lib/components/page_route.dart new file mode 100644 index 0000000..f0c03f7 --- /dev/null +++ b/lib/components/page_route.dart @@ -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 extends PageRoute 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 on PageRoute { + /// 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 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 animation, + Animation 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(PageRoute 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 animation, + Animation secondaryAnimation, Widget child) { + return DrillInPageTransition( + animation: CurvedAnimation( + parent: animation, + curve: FluentTheme.of(context).animationCurve, + ), + child: enableIOSGesture + ? IOSBackGestureDetector( + gestureWidth: _kBackGestureWidth, + enabledCallback: () => _isPopGestureEnabled(this), + onStartPopGesture: () => _startPopGesture(this), + child: child) + : child, + ); + } + + IOSBackGestureController _startPopGesture(PageRoute 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 createState() => _IOSBackGestureDetectorState(); +} + +class _IOSBackGestureDetectorState extends State { + 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.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)); + } +} diff --git a/lib/components/segmented_button.dart b/lib/components/segmented_button.dart new file mode 100644 index 0000000..2c7929d --- /dev/null +++ b/lib/components/segmented_button.dart @@ -0,0 +1,72 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:pixes/foundation/app.dart'; + +import 'color_scheme.dart'; + +class SegmentedButton extends StatelessWidget { + const SegmentedButton( + {required this.options, + required this.value, + required this.onPressed, + super.key}); + + final List> 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 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 { + final T key; + final String text; + + const SegmentedButtonOption(this.key, this.text); +} diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart new file mode 100644 index 0000000..a03bce4 --- /dev/null +++ b/lib/foundation/app.dart @@ -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(); +} + +// ignore: non_constant_identifier_names +final App = _App(); diff --git a/lib/foundation/cache_manager.dart b/lib/foundation/cache_manager.dart new file mode 100644 index 0000000..3dec211 --- /dev/null +++ b/lib/foundation/cache_manager.dart @@ -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 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 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 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 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 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 clear() async { + await Directory(cachePath).delete(recursive: true); + Directory(cachePath).createSync(recursive: true); + _db.execute(''' + DELETE FROM cache + '''); + _currentSize = 0; + } + + Future 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 _buffer = []; + + Future writeBytes(List data) async{ + _buffer.addAll(data); + if(_buffer.length > 1024 * 1024){ + await file.writeAsBytes(_buffer, mode: FileMode.append); + _buffer.clear(); + } + } + + Future 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 cancel() async{ + await file.deleteIfExists(); + } +} \ No newline at end of file diff --git a/lib/foundation/image_provider.dart b/lib/foundation/image_provider.dart new file mode 100644 index 0000000..946fc63 --- /dev/null +++ b/lib/foundation/image_provider.dart @@ -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> + extends ImageProvider { + const BaseImageProvider(); + + @override + ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) { + final chunkEvents = StreamController(); + return MultiFrameImageStreamCompleter( + codec: _loadBufferAsync(key, chunkEvents, decode), + chunkEvents: chunkEvents.stream, + scale: 1.0, + informationCollector: () sync* { + yield DiagnosticsProperty( + 'Image provider: $this \n Image key: $key', + this, + style: DiagnosticsTreeStyle.errorProperty, + ); + }, + ); + } + + Future _loadBufferAsync( + T key, + StreamController 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 load(StreamController chunkEvents); + + String get key; + + @override + bool operator ==(Object other) { + return other is BaseImageProvider && key == other.key; + } + + @override + int get hashCode => key.hashCode; + + @override + String toString() { + return "$runtimeType($key)"; + } +} + +typedef FileDecoderCallback = Future Function(Uint8List); + +class CachedImageProvider extends BaseImageProvider { + final String url; + + CachedImageProvider(this.url); + + @override + String get key => url; + + @override + Future load(StreamController 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 = []; + 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 obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } +} diff --git a/lib/foundation/log.dart b/lib/foundation/log.dart new file mode 100644 index 0000000..c627552 --- /dev/null +++ b/lib/foundation/log.dart @@ -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 _logs = []; + + static List 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; + } +} diff --git a/lib/foundation/navigation.dart b/lib/foundation/navigation.dart new file mode 100644 index 0000000..1c78a49 --- /dev/null +++ b/lib/foundation/navigation.dart @@ -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? result]) { + Navigator.of(this).pop(result); + } + + Future to(Widget Function() builder) { + return Navigator.of(this) + .push(AppPageRoute(builder: (context) => builder())); + } + + void showToast({required String message, IconData? icon}) { + overlay.showToast(this, message: message, icon: icon); + } +} diff --git a/lib/foundation/pair.dart b/lib/foundation/pair.dart new file mode 100644 index 0000000..05a602e --- /dev/null +++ b/lib/foundation/pair.dart @@ -0,0 +1,9 @@ +class Pair{ + M left; + V right; + + Pair(this.left, this.right); + + Pair.fromMap(Map map, M key): left = key, right = map[key] + ?? (throw Exception("Pair not found")); +} \ No newline at end of file diff --git a/lib/foundation/state_controller.dart b/lib/foundation/state_controller.dart new file mode 100644 index 0000000..9af6a63 --- /dev/null +++ b/lib/foundation/state_controller.dart @@ -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 = []; + + static T put(T controller, {Object? tag, bool autoRemove = false}){ + _controllers.add(StateControllerWrapped(controller, autoRemove, tag)); + return controller; + } + + static T putIfNotExists(T controller, {Object? tag, bool autoRemove = false}){ + return findOrNull(tag: tag) ?? put(controller, tag: tag, autoRemove: autoRemove); + } + + static T find({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({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([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> stateUpdaters = []; + + void update([List? 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 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 createState() => _StateBuilderState(); +} + +class _StateBuilderState extends State { + late T controller; + + @override + void initState() { + if(widget.init != null) { + StateController.put(widget.init!, tag: widget.tag, autoRemove: true); + } + try { + controller = StateController.find(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(widget.tag, true); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.builderWrapped(controller); +} + +abstract class StateWithController extends State{ + 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; +} \ No newline at end of file diff --git a/lib/foundation/widget_utils.dart b/lib/foundation/widget_utils.dart new file mode 100644 index 0000000..3e463ef --- /dev/null +++ b/lib/foundation/widget_utils.dart @@ -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); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..1559e8b --- /dev/null +++ b/lib/main.dart @@ -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( + 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); + }); + }); + } +} diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart new file mode 100644 index 0000000..284dc45 --- /dev/null +++ b/lib/network/app_dio.dart @@ -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 = { + 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 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) { + content = "\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> request(String path, + {Object? data, + Map? 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; + } +} diff --git a/lib/network/download.dart b/lib/network/download.dart new file mode 100644 index 0000000..66910f0 --- /dev/null +++ b/lib/network/download.dart @@ -0,0 +1,5 @@ +import 'package:pixes/network/network.dart'; + +extension IllustExt on Illust { + bool get downloaded => false; +} \ No newline at end of file diff --git a/lib/network/models.dart b/lib/network/models.dart new file mode 100644 index 0000000..b3478d3 --- /dev/null +++ b/lib/network/models.dart @@ -0,0 +1,215 @@ +class Account { + String accessToken; + String refreshToken; + final User user; + + Account(this.accessToken, this.refreshToken, this.user); + + Account.fromJson(Map json) + : accessToken = json['access_token'], + refreshToken = json['refresh_token'], + user = User.fromJson(json['user']); + + Map 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 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 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 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 images; + final String caption; + final int restrict; + final IllustAuthor author; + final List 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 json) + : id = json['id'], + title = json['title'], + type = json['type'], + images = (() { + List 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); +} diff --git a/lib/network/network.dart b/lib/network/network.dart new file mode 100644 index 0000000..9fd94e6 --- /dev/null +++ b/lib/network/network.dart @@ -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 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 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> loginWithCode(String code) async { + try { + var res = await dio.post("$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> refreshToken() async { + try { + var res = await dio.post("$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>> apiGet(String path, {Map? query}) async { + try { + if(!path.startsWith("http")) { + path = "$baseUrl$path"; + } + final res = await dio.get>(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>> apiPost(String path, {Map? query, Map? data}) async { + try { + if(!path.startsWith("http")) { + path = "$baseUrl$path"; + } + final res = await dio.post>(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> 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>> 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>> 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> 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> 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>> 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"]) + )))); + } + } +} diff --git a/lib/network/res.dart b/lib/network/res.dart new file mode 100644 index 0000000..f9ebee7 --- /dev/null +++ b/lib/network/res.dart @@ -0,0 +1,39 @@ +import 'package:flutter/cupertino.dart'; + +@immutable +class Res{ + ///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; +} \ No newline at end of file diff --git a/lib/pages/bookmarks.dart b/lib/pages/bookmarks.dart new file mode 100644 index 0000000..52eabee --- /dev/null +++ b/lib/pages/bookmarks.dart @@ -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 createState() => _BookMarkedArtworkPageState(); +} + +class _BookMarkedArtworkPageState extends State{ + 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 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>> 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; + } +} + diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart new file mode 100644 index 0000000..9b16de9 --- /dev/null +++ b/lib/pages/explore_page.dart @@ -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"), + ); + } +} diff --git a/lib/pages/illust_detail_page.dart b/lib/pages/illust_detail_page.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart new file mode 100644 index 0000000..66282c3 --- /dev/null +++ b/lib/pages/illust_page.dart @@ -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 createState() => _IllustPageState(); +} + +class _IllustPageState extends State { + @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 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); + } +} diff --git a/lib/pages/image_page.dart b/lib/pages/image_page.dart new file mode 100644 index 0000000..31efe65 --- /dev/null +++ b/lib/pages/image_page.dart @@ -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 createState() => _ImagePageState(); +} + +class _ImagePageState extends State 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),), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart new file mode 100644 index 0000000..1245cb9 --- /dev/null +++ b/lib/pages/login_page.dart @@ -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 createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + 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(); + } + } +} diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart new file mode 100644 index 0000000..834c451 --- /dev/null +++ b/lib/pages/main_page.dart @@ -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 createState() => _MainPageState(); +} + +class _MainPageState extends State with WindowListener { + final navigatorKey = GlobalKey(); + + 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 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 navigatorKey; + + @override + State<_BackButton> createState() => _BackButtonState(); +} + +class _BackButtonState extends State<_BackButton> { + GlobalKey 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( + future: windowManager.isMaximized(), + builder: (BuildContext context, AsyncSnapshot 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 createState() => _WindowButtonState(); +} + +class _WindowButtonState extends State { + 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; diff --git a/lib/pages/ranking.dart b/lib/pages/ranking.dart new file mode 100644 index 0000000..5b22329 --- /dev/null +++ b/lib/pages/ranking.dart @@ -0,0 +1,15 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class RankingPage extends StatefulWidget { + const RankingPage({super.key}); + + @override + State createState() => _RankingPageState(); +} + +class _RankingPageState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/pages/recommendation_page.dart b/lib/pages/recommendation_page.dart new file mode 100644 index 0000000..5f00760 --- /dev/null +++ b/lib/pages/recommendation_page.dart @@ -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 createState() => _RecommendationPageState(); +} + +class _RecommendationPageState extends MultiPageLoadingState { + @override + Widget buildContent(BuildContext context, final List 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>> loadData(page) { + return Network().getRecommendedIllusts(); + } +} \ No newline at end of file diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart new file mode 100644 index 0000000..c7b58d6 --- /dev/null +++ b/lib/pages/search_page.dart @@ -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 createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + 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> { + @override + Widget buildContent(BuildContext context, List 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>> loadData() { + return Network().getHotTags(); + } +} + + +class SearchResultPage extends StatefulWidget { + const SearchResultPage(this.keyword, {super.key}); + + final String keyword; + + @override + State createState() => _SearchResultPageState(); +} + +class _SearchResultPageState extends State { + @override + Widget build(BuildContext context) { + return const ScaffoldPage(); + } +} + diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..ce08dbb --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,15 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} \ No newline at end of file diff --git a/lib/pages/user_info_page.dart b/lib/pages/user_info_page.dart new file mode 100644 index 0000000..4fd2972 --- /dev/null +++ b/lib/pages/user_info_page.dart @@ -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 createState() => _UserInfoPageState(); +} + +class _UserInfoPageState extends LoadingState { + @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> loadData() { + return Network().getUserDetails(widget.id); + } + +} diff --git a/lib/utils/app_links.dart b/lib/utils/app_links.dart new file mode 100644 index 0000000..a86a781 --- /dev/null +++ b/lib/utils/app_links.dart @@ -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 _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; + } + }); +} diff --git a/lib/utils/ext.dart b/lib/utils/ext.dart new file mode 100644 index 0000000..fb30a1d --- /dev/null +++ b/lib/utils/ext.dart @@ -0,0 +1,86 @@ +extension ListExt on List{ + /// Remove all blank value and return the list. + List getNoBlankList(){ + List 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 toList() => [this]; + + String _nums(){ + String res = ""; + for(int i=0; i _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; +} \ No newline at end of file diff --git a/lib/utils/io.dart b/lib/utils/io.dart new file mode 100644 index 0000000..4edfcb7 --- /dev/null +++ b/lib/utils/io.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +extension FSExt on FileSystemEntity { + Future 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; + } +} \ No newline at end of file diff --git a/lib/utils/mouse_listener.dart b/lib/utils/mouse_listener.dart new file mode 100644 index 0000000..088361f --- /dev/null +++ b/lib/utils/mouse_listener.dart @@ -0,0 +1,26 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import '../foundation/app.dart'; + +void mouseSideButtonCallback(GlobalKey key){ + if(App.rootNavigatorKey.currentState?.canPop() ?? false) { + App.rootNavigatorKey.currentState?.pop(); + return; + } + if(key.currentState?.canPop() ?? false){ + key.currentState?.pop(); + } +} + +///监听鼠标侧键, 若为下键, 则调用返回 +void listenMouseSideButtonToBack(GlobalKey key) async{ + if(!App.isWindows){ + return; + } + const channel = EventChannel("pixes/mouse"); + await for(var res in channel.receiveBroadcastStream()){ + if(res == 0){ + mouseSideButtonCallback(key); + } + } +} \ No newline at end of file diff --git a/lib/utils/translation.dart b/lib/utils/translation.dart new file mode 100644 index 0000000..64d3f7c --- /dev/null +++ b/lib/utils/translation.dart @@ -0,0 +1,14 @@ +import 'package:pixes/foundation/app.dart'; + +extension Translation on String { + String get tl { + var locale = App.locale; + return translation["${locale.languageCode}_${locale.countryCode}"]?[this] ?? + this; + } + + static const translation = >{ + "zh_CN": {}, + "zh_TW": {}, + }; +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..55d081e --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "pixes") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.github.wgh136.pixes") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e6d5fc3 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) system_theme_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin"); + system_theme_plugin_register_with_registrar(system_theme_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..0c95e46 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + gtk + screen_retriever + sqlite3_flutter_libs + system_theme + url_launcher_linux + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..d397561 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,131 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "pixes"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "pixes"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_realize(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return FALSE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..771a1f8 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import path_provider_foundation +import screen_retriever +import sqlite3_flutter_libs +import system_theme +import url_launcher_macos +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dde8991 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* pixes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "pixes.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* pixes.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* pixes.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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)/pixes.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/pixes"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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)/pixes.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/pixes"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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)/pixes.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/pixes"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..97b73f6 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..218f93e --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return false + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..6422325 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = pixes + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 com.github.wgh136. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..2afed4d --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,44 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + CFBundleURLTypes + + + CFBundleURLName + + pixiv + CFBundleURLSchemes + + pixiv + + + + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..cb59c5e --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,20 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } + + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +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. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..c19992b --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,536 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "1c2b9e9c56d80d17610bcbd111b37187875c5d0ded8654caa1bda14ea753d001" + url: "https://pub.dev" + source: hosted + version: "6.0.1" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.dev" + source: hosted + version: "5.4.3+1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + sha256: a8c76cb501303d108cb9bd33e516da7cfd078031ff427d68eab6069bf4492a2c + url: "https://pub.dev" + source: hosted + version: "4.8.7" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + math_expressions: + dependency: transitive + description: + name: math_expressions + sha256: db0b72d867491c4e53a1c773e2708d5d6e94bbe06be07080fc9f896766b9cd3d + url: "https://pub.dev" + source: hosted + version: "2.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + photo_view: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "97de36fa8c500c18037f675c122785b193559e09" + url: "https://github.com/wgh136/photo_view" + source: git + version: "0.14.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + url: "https://pub.dev" + source: hosted + version: "0.1.9" + scroll_pos: + dependency: transitive + description: + name: scroll_pos + sha256: cebf602b2dd939de6832bb902ffefb574608d1b84f420b82b381a4007d3c1e1b + url: "https://pub.dev" + source: hosted + version: "0.5.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: fb2a106a2ea6042fe57de2c47074cc31539a941819c91e105b864744605da3f5 + url: "https://pub.dev" + source: hosted + version: "0.5.21" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + system_theme: + dependency: "direct main" + description: + name: system_theme + sha256: "1f208db140a3d1e1eac2034b54920d95699c1534df576ced44b3312c5de3975f" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + system_theme_web: + dependency: transitive + description: + name: system_theme_web + sha256: "7566f5a928f6d28d7a60c97bea8a851d1c6bc9b86a4df2366230a97458489219" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + win32_registry: + dependency: "direct main" + description: + name: win32_registry + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + url: "https://pub.dev" + source: hosted + version: "0.3.8" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" +sdks: + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..1407899 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,104 @@ +name: pixes +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.3.4 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + window_manager: ^0.3.8 + fluent_ui: ^4.8.7 + system_theme: ^2.3.1 + dio: ^5.4.3 + crypto: + intl: + path_provider: + url_launcher: ^6.1.8 + app_links: ^6.0.1 + win32_registry: ^1.1.3 + flutter_staggered_grid_view: ^0.7.0 + sqlite3: ^2.4.3 + sqlite3_flutter_libs: any + photo_view: + git: + url: https://github.com/wgh136/photo_view + ref: main +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..f198403 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:pixes/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..f1d772c --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(pixes LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "pixes") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..da4bd0b --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,29 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + SystemThemePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemThemePlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..d071d54 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + screen_retriever + sqlite3_flutter_libs + system_theme + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..d69bde2 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.github.wgh136" "\0" + VALUE "FileDescription", "pixes" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "pixes" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.github.wgh136. All rights reserved." "\0" + VALUE "OriginalFilename", "pixes.exe" "\0" + VALUE "ProductName", "pixes" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..2206cfd --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,117 @@ +#include "flutter_window.h" +#include +#include +#include +#include +#include + +#include "flutter/generated_plugin_registrant.h" + +std::unique_ptr>&& mouseEvents = nullptr; + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + + //监听鼠标侧键的EventChannel + const auto channelName = "pixes/mouse"; + flutter::EventChannel<> channel2( + flutter_controller_->engine()->messenger(), channelName, + &flutter::StandardMethodCodec::GetInstance() + ); + + auto eventHandler = std::make_unique< + flutter::StreamHandlerFunctions>( + []( + const flutter::EncodableValue* arguments, + std::unique_ptr>&& events){ + mouseEvents = std::move(events); + return nullptr; + }, + [](const flutter::EncodableValue* arguments) + -> std::unique_ptr> { + mouseEvents = nullptr; + return nullptr; + }); + + channel2.SetStreamHandler(std::move(eventHandler)); + + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + //this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +void mouse_side_button_listener(unsigned int input) +{ + if(mouseEvents != nullptr) + { + mouseEvents->Success(static_cast(input)); + } +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + UINT button = GET_XBUTTON_WPARAM(wparam); + if (button == XBUTTON1 && message == 528) + { + mouse_side_button_listener(0); + } + else if (button == XBUTTON2 && message == 528) + { + mouse_side_button_listener(1); + } + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..6dff865 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,82 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" +#include "app_links/app_links_plugin_c_api.h" + +bool SendAppLinkToInstance(const std::wstring& title) { + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + + switch(place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + if (SendAppLinkToInstance(L"pixes")) { + return EXIT_SUCCESS; + } + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"pixes", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_