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..e720147 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# 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: "b0850beeb25f6d5b10426284f506557f66181b36" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 + - platform: android + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 + + # 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/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1edebac --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "configurations": [ + { + "name": "Flutter", + "type": "dart", + "request": "launch", + "program": "lib/main.dart", + "args": [ + "--dart-define=cronetHttpNoPlay=true" + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..153deba --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +

+ icon +

+ +
+ +# Railgun- + +✨ Use Bilibili As Your Audio Player ✨ + +
+ +Railgun- is a mobile application designed to provide playback functionality for Bilibili audio content. This application is built using the Flutter framework, ensuring efficient audio playback and a user-friendly interface. + +## Features + +- Browse and search Bilibili audio content +- Play and pause audio +- Create and manage playlists +- Background playback support + +

+ screenshot_1 + screenshot_2 + screenshot_3 +

+ +

+ screenshot_4 + screenshot_5 + screenshot_6 +

+ +## Compatibility + +| Android | iOS | +| :-----: | :-: | +| ✔️ | - | + +## Quick Start + +1. Install the Flutter SDK. +2. Clone this repository. +3. Run `flutter pub get` in the project's root directory to install dependencies. +4. Connect an Android device or start an emulator. +5. Run `flutter run` to launch the application. + +## Disclaimer + +This project is for learning purposes only. Please do not use it for other purposes. 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..5e1cd02 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + 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.ezer.railgun_minus" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.ezer.railgun_minus" + // 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. + minSdk = flutter.minSdkVersion + targetSdk = 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 = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..3b7547d --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..99249b6 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/railgun_minus/MainActivity.kt b/android/app/src/main/kotlin/com/example/railgun_minus/MainActivity.kt new file mode 100644 index 0000000..d172c28 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/railgun_minus/MainActivity.kt @@ -0,0 +1,5 @@ +package com.ezer.railgun_minus + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png new file mode 100644 index 0000000..0f73141 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..0f73141 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png new file mode 100644 index 0000000..1745a95 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..1745a95 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png new file mode 100644 index 0000000..51a458f Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/splash.png b/android/app/src/main/res/drawable-night-hdpi/splash.png new file mode 100644 index 0000000..51a458f Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png new file mode 100644 index 0000000..8a02ecb Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/splash.png b/android/app/src/main/res/drawable-night-mdpi/splash.png new file mode 100644 index 0000000..8a02ecb Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-v21/background.png b/android/app/src/main/res/drawable-night-v21/background.png new file mode 100644 index 0000000..71e9c81 Binary files /dev/null and b/android/app/src/main/res/drawable-night-v21/background.png differ diff --git a/android/app/src/main/res/drawable-night-v21/launch_background.xml b/android/app/src/main/res/drawable-night-v21/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-night-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png new file mode 100644 index 0000000..b681273 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/splash.png b/android/app/src/main/res/drawable-night-xhdpi/splash.png new file mode 100644 index 0000000..b681273 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png new file mode 100644 index 0000000..9316ed5 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/splash.png b/android/app/src/main/res/drawable-night-xxhdpi/splash.png new file mode 100644 index 0000000..9316ed5 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png new file mode 100644 index 0000000..07c7e44 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/splash.png new file mode 100644 index 0000000..07c7e44 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night/background.png b/android/app/src/main/res/drawable-night/background.png new file mode 100644 index 0000000..71e9c81 Binary files /dev/null and b/android/app/src/main/res/drawable-night/background.png differ diff --git a/android/app/src/main/res/drawable-night/launch_background.xml b/android/app/src/main/res/drawable-night/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-night/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..3107d37 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ 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..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png new file mode 100644 index 0000000..07efdcc Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..07efdcc Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png new file mode 100644 index 0000000..6e6d4cb Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..6e6d4cb Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 0000000..d34cd71 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..d34cd71 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..3107d37 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ 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..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + 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..f4ca03c 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..e3d3c2f 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..6e82ab7 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..f8d98b4 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..55076c3 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-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..2ddcec1 --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + 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..dbc9ea9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..e437c38 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + 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..0d1fa8f --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..eb5f25b --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 127.0.0.1 + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..9a612f8 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /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..b4dddc5 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file 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..536165d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,25 @@ +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 + }() + + includeBuild("$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/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..8ef79b3 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icon_dark.png b/assets/icon_dark.png new file mode 100644 index 0000000..0daf127 Binary files /dev/null and b/assets/icon_dark.png differ diff --git a/assets/screenshot_1.jpg b/assets/screenshot_1.jpg new file mode 100644 index 0000000..1640056 Binary files /dev/null and b/assets/screenshot_1.jpg differ diff --git a/assets/screenshot_2.jpg b/assets/screenshot_2.jpg new file mode 100644 index 0000000..8799772 Binary files /dev/null and b/assets/screenshot_2.jpg differ diff --git a/assets/screenshot_3.jpg b/assets/screenshot_3.jpg new file mode 100644 index 0000000..f0a7b65 Binary files /dev/null and b/assets/screenshot_3.jpg differ diff --git a/assets/screenshot_4.jpg b/assets/screenshot_4.jpg new file mode 100644 index 0000000..526842a Binary files /dev/null and b/assets/screenshot_4.jpg differ diff --git a/assets/screenshot_5.jpg b/assets/screenshot_5.jpg new file mode 100644 index 0000000..a2d8578 Binary files /dev/null and b/assets/screenshot_5.jpg differ diff --git a/assets/screenshot_6.jpg b/assets/screenshot_6.jpg new file mode 100644 index 0000000..7d334f8 Binary files /dev/null and b/assets/screenshot_6.jpg differ 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..b99329c --- /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.example.railgunMinus; + 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.example.railgunMinus.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.example.railgunMinus.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.example.railgunMinus.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 = AppIcon; + 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 = AppIcon; + 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.example.railgunMinus; + 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.example.railgunMinus; + 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..9074fee --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@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..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"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":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"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":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@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"}} \ No newline at end of file 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..ae21edb 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..a1ef078 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..e21839e 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..ec9a2e3 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..b60120f 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..44d9a51 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..cfdeabf 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..e21839e 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..27b79d9 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..8a913c4 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-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..6ed931b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..7bf4c13 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..476ebb3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..68ba773 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.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..8a913c4 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..86a4fe1 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-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..ddf6d2b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..50fc12e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.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..63c5244 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..c5ebb11 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..a99169d 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/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json new file mode 100644 index 0000000..8bb185b --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "darkbackground.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png new file mode 100644 index 0000000..3107d37 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png new file mode 100644 index 0000000..71e9c81 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.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..f3387d4 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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..1745a95 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..07efdcc 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..6e6d4cb 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/LaunchImageDark.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png new file mode 100644 index 0000000..8a02ecb Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png new file mode 100644 index 0000000..b681273 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png new file mode 100644 index 0000000..9316ed5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@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..2fd157a --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..6914be4 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Railgun Minus + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + railgun_minus + 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 + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + + + 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/main.dart b/lib/main.dart new file mode 100644 index 0000000..1abcea6 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// import 'package:just_audio_background/just_audio_background.dart'; + +import 'src/router.dart'; +import 'src/services.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await SystemChrome.setPreferredOrientations( + [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); + await SeamlessJustAudioBackground.init( + androidNotificationChannelId: 'com.ezer.railgun.channel.audio', + androidNotificationChannelName: 'Railgun-', + androidNotificationOngoing: true, + ); + await AudioPlayApi.init(); + await SettingApi.init(); + await RemoteApi.init(); + runApp(const App()); +} + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: appRouter, + theme: ThemeData.from( + colorScheme: const ColorScheme.light( + primary: Colors.indigoAccent, + secondary: Colors.cyanAccent, + ), + ), + darkTheme: ThemeData.from( + colorScheme: const ColorScheme.dark( + primary: Colors.indigoAccent, + secondary: Colors.cyanAccent, + ), + ), + ); + } +} diff --git a/lib/src/component/app_search_bar.dart b/lib/src/component/app_search_bar.dart new file mode 100644 index 0000000..d3ba03c --- /dev/null +++ b/lib/src/component/app_search_bar.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +import '../services.dart'; + +class AppSearchBar extends StatefulWidget implements PreferredSizeWidget { + const AppSearchBar({ + super.key, + this.height = 60, + this.padding = const EdgeInsets.fromLTRB(16, 12, 16, 4), + this.searchText = '', + required this.controller, + required this.onSubmitted, + }); + + final double height; + final EdgeInsets padding; + final String searchText; + final TextEditingController controller; + final void Function(String) onSubmitted; + + @override + State createState() => _AppSearchBarState(); + + @override + Size get preferredSize => Size.fromHeight(height); +} + +class _AppSearchBarState extends State { + late final _controller = widget.controller; + final _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _controller.text = widget.searchText; + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.padding, + child: SearchBar( + controller: _controller, + focusNode: _focusNode, + leading: const Opacity(opacity: .6, child: Icon(Icons.search)), + shadowColor: const WidgetStatePropertyAll(Colors.transparent), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + side: BorderSide( + color: (Theme.of(context).brightness == Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColorLight) + .withOpacity(.85), + width: 1.6), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ), + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 16)), + onTapOutside: (event) => _focusNode.unfocus(), + onSubmitted: (value) { + if (value == '') { + return; + } + if (value.startsWith('>')) { + var commands = value.split(RegExp(r'\s+')); + if (commands[0] == '>' && commands.length > 1) { + switch (commands[1]) { + case 'up': + if (commands.length > 2) { + switch (commands[2]) { + case 'add': + switch (commands.length) { + case 4: + case 5: + case 6: + var uid = int.tryParse(commands[3]); + var tag = + commands.length > 4 ? commands[4] : null; + var pattern = + commands.length > 5 ? commands[5] : null; + if (uid != null) { + SettingApi.addUp( + uid: uid, + tag: tag, + pattern: pattern, + ); + _controller.clear(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Up $uid added')), + ); + return; + } + } + case 'remove': + if (commands.length == 4) { + var uid = int.tryParse(commands[3]); + if (uid != null) { + SettingApi.removeUp(uid); + _controller.clear(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Up $uid removed')), + ); + return; + } + } + default: + } + } + default: + } + } + } + widget.onSubmitted(value); + }), + ); + } +} diff --git a/lib/src/component/audio_view.dart b/lib/src/component/audio_view.dart new file mode 100644 index 0000000..eccc8c3 --- /dev/null +++ b/lib/src/component/audio_view.dart @@ -0,0 +1,544 @@ +import 'package:flutter/material.dart'; + +import '../models.dart'; +import '../services.dart'; +import '../utils.dart'; + +class VerticalOverlayShape extends SliderComponentShape { + const VerticalOverlayShape({required this.height}); + + final double height; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size(0, height); + } + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow, + }) { + final Paint paint = Paint() + ..color = sliderTheme.overlayColor! + ..style = PaintingStyle.fill; + + final Rect rect = Rect.fromCenter(center: center, width: 0, height: height); + context.canvas.drawRect(rect, paint); + } +} + +class AudioView extends StatefulWidget { + const AudioView({super.key}); + + @override + _AudioViewState createState() => _AudioViewState(); +} + +class _AudioViewState extends State with TickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + + final audioSummariesLength = AudioPlayApi.audioSummaries?.length ?? 0; + var currIndex = + AudioPlayApi.currentIndex < 0 ? 0 : AudioPlayApi.currentIndex; + if (audioSummariesLength > 1) { + currIndex++; + } + + _tabController = TabController( + length: audioSummariesLength > 1 + ? audioSummariesLength + 2 + : audioSummariesLength, + initialIndex: currIndex, + vsync: this, + ); + _tabController.addListener(_syncAudioState); + + AudioSequenceNotifier.registerListener(_updateAudioSequence); + AudioPlayingNotifier.registerListener(_updateState); + } + + @override + void dispose() { + AudioSequenceNotifier.unregisterListener(_updateAudioSequence); + AudioPlayingNotifier.unregisterListener(_updateState); + _tabController.removeListener(_syncAudioState); + _tabController.dispose(); + super.dispose(); + } + + void _updateAudioSequence() { + final audioSummariesLength = AudioPlayApi.audioSummaries?.length ?? 0; + var currIndex = + AudioPlayApi.currentIndex < 0 ? 0 : AudioPlayApi.currentIndex; + // TODO: Unknowing currIndex update not in time + if (currIndex > audioSummariesLength || + (currIndex == audioSummariesLength && currIndex > 0)) { + return; + } + + if (audioSummariesLength > 1) { + currIndex++; + } + setState(() { + _tabController.dispose(); + _tabController = TabController( + length: audioSummariesLength > 1 + ? audioSummariesLength + 2 + : audioSummariesLength, + initialIndex: currIndex, + vsync: this, + ); + _tabController.addListener(_syncAudioState); + }); + } + + void _updateState() => setState(() {}); + + Future _syncAudioState() async { + var currIndex = _tabController.index; + var prevIndex = AudioPlayApi.currentIndex; + final audioSummariesLength = AudioPlayApi.audioSummaries?.length ?? 0; + + if (audioSummariesLength > 1) { + prevIndex++; + if (currIndex == 0) { + currIndex = audioSummariesLength; + await AudioPlayApi.pause(); + await AudioPlayApi.seekToPrevious(); + AudioPlayApi.play(); + return _tabController.animateTo(currIndex); + } else if (currIndex == audioSummariesLength + 1) { + currIndex = 1; + await AudioPlayApi.pause(); + await AudioPlayApi.seekToNext(); + AudioPlayApi.play(); + return _tabController.animateTo(currIndex); + } + } + + if (currIndex == prevIndex) { + return; + } + + await AudioPlayApi.pause(); + if (prevIndex == 0) { + return; + } + + currIndex < prevIndex + ? await AudioPlayApi.seekToPrevious() + : await AudioPlayApi.seekToNext(); + + return AudioPlayApi.play(); + } + + @override + Widget build(BuildContext context) { + final currAudioSummary = AudioPlayApi.currentAudioSummary; + final audioSummaries = AudioPlayApi.audioSummaries ?? []; + + final previewImage = Stack( + alignment: Alignment.center, + children: [ + (currAudioSummary?.isValid ?? false) + ? ClipOval( + child: AspectRatio( + aspectRatio: 1, + child: Stack( + fit: StackFit.expand, + children: [ + Image.memory( + currAudioSummary!.pic, + fit: BoxFit.cover, + ), + Container(color: Colors.black26), + ], + ), + ), + ) + : AspectRatio( + aspectRatio: 1, + child: Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.75), + shape: BoxShape.circle, + ), + ), + ), + Icon( + AudioPlayApi.isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 20, + ), + ], + ); + + final audioHandler = Flexible( + child: TabBarView( + controller: _tabController, + children: [ + if (audioSummaries.length > 1) ...[ + AudioTabView(audioSummary: audioSummaries.last), + for (var audioSummary in audioSummaries) + AudioTabView(audioSummary: audioSummary), + AudioTabView(audioSummary: audioSummaries.first), + ] else + for (var audioSummary in audioSummaries) + AudioTabView(audioSummary: audioSummary), + ], + ), + ); + + return currAudioSummary?.isValid ?? false + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + const TrackProgress(), + SizedBox( + height: 42, + child: Row( + children: [ + Flexible( + child: InkWell( + onTap: () { + if (currAudioSummary?.isValid ?? false) { + setState(() { + AudioPlayApi.isPlaying + ? AudioPlayApi.pause() + : AudioPlayApi.play(); + }); + } + }, + onLongPress: () => showModalBottomSheet( + context: context, + builder: (context) => const AudioPlaylistView(), + ), + child: Row( + children: [ + const SizedBox(width: 10), + previewImage, + const SizedBox(width: 8), + audioHandler, + const PlayModeButton(), + const SizedBox(width: 4), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ) + : const SizedBox.shrink(); + } +} + +class AudioTabView extends StatelessWidget { + const AudioTabView({ + super.key, + required this.audioSummary, + }); + + final AudioSummary audioSummary; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + Display.formatTitle(audioSummary.title, simplify: true), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2.5), + const Opacity( + opacity: .4, + child: TrackTime(), + ), + ], + ); + } +} + +class AudioPlaylistView extends StatefulWidget { + const AudioPlaylistView({ + super.key, + }); + + @override + State createState() => _AudioPlaylistViewState(); +} + +class _AudioPlaylistViewState extends State { + @override + void initState() { + super.initState(); + AudioSequenceNotifier.registerListener(_updateState); + } + + @override + void dispose() { + AudioSequenceNotifier.unregisterListener(_updateState); + super.dispose(); + } + + void _updateState() => setState(() {}); + + @override + Widget build(BuildContext context) { + final currAudioSummaries = AudioPlayApi.audioSummaries ?? []; + final currIndex = AudioPlayApi.currentIndex; + return Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 16), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 4, + width: 36, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.25), + borderRadius: BorderRadius.circular(99), + ), + ), + const SizedBox(height: 16), + for (var entry in currAudioSummaries.asMap().entries) + Container( + color: entry.key == currIndex + ? Theme.of(context).colorScheme.primary.withOpacity(.1) + : null, + child: InkWell( + onTap: () async { + await AudioPlayApi.pause(); + await AudioPlayApi.seekTo(entry.value); + AudioPlayApi.play(); + // setState(() {}); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 24), + child: Row( + children: [ + Flexible( + child: Text( + Display.formatTitle( + entry.value.title, + simplify: true, + ), + style: TextStyle( + color: entry.key == currIndex + ? Theme.of(context).colorScheme.primary + : null, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Opacity( + opacity: .4, + child: Text( + ' · ${Display.formatTitle( + entry.value.author, + simplify: true, + )}', + style: TextStyle( + color: entry.key == currIndex + ? Theme.of(context).colorScheme.primary + : null, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 8), + child: Opacity( + opacity: .4, + child: IconButton( + icon: const Icon(Icons.close), + iconSize: 20, + highlightColor: Colors.transparent, + onPressed: () async { + await AudioPlayApi.removeAt(entry.key); + if (AudioPlayApi.audioSummaries?.isEmpty ?? + true) { + setState(() { + Navigator.pop(context); + }); + } + }), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class TrackTime extends StatefulWidget { + const TrackTime({ + super.key, + }); + + @override + State createState() => _TrackTimeState(); +} + +class _TrackTimeState extends State { + @override + void initState() { + super.initState(); + AudioPositionNotifier.registerListener(_updateState); + } + + @override + void dispose() { + AudioPositionNotifier.unregisterListener(_updateState); + super.dispose(); + } + + void _updateState() => setState(() {}); + + @override + Widget build(BuildContext context) { + final position = AudioPlayApi.position; + final positionMinutes = position.inMinutes; + final positionSeconds = position.inSeconds.remainder(60); + final duration = AudioPlayApi.duration; + final durationMinutes = duration?.inMinutes ?? 0; + final durationSeconds = duration?.inSeconds.remainder(60) ?? 0; + + return Text( + '${Display.formatDuration('$positionMinutes:$positionSeconds')} / ${Display.formatDuration('$durationMinutes:$durationSeconds')}', + style: const TextStyle(fontSize: 10), + ); + } +} + +class PlayModeButton extends StatefulWidget { + const PlayModeButton({ + super.key, + }); + + @override + State createState() => _PlayModeButtonState(); +} + +class _PlayModeButtonState extends State { + @override + void initState() { + super.initState(); + AudioPlayModeNotifier.registerListener(_updateState); + } + + @override + void dispose() { + AudioPlayModeNotifier.unregisterListener(_updateState); + super.dispose(); + } + + void _updateState() => setState(() {}); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon( + switch (AudioPlayApi.currentPlayMode) { + PlayMode.all => Icons.repeat, + PlayMode.one => Icons.repeat_one, + PlayMode.shuffle => Icons.shuffle, + }, + ), + onPressed: () => setState(() { + switch (AudioPlayApi.currentPlayMode) { + case PlayMode.all: + AudioPlayApi.setPlayMode(PlayMode.one); + case PlayMode.one: + AudioPlayApi.setPlayMode(PlayMode.shuffle); + case PlayMode.shuffle: + AudioPlayApi.setPlayMode(PlayMode.all); + } + }), + ); + } +} + +class TrackProgress extends StatefulWidget { + const TrackProgress({ + super.key, + }); + + @override + State createState() => _TrackProgressState(); +} + +class _TrackProgressState extends State { + @override + void initState() { + super.initState(); + AudioPositionNotifier.registerListener(_updateState); + } + + @override + void dispose() { + AudioPositionNotifier.unregisterListener(_updateState); + super.dispose(); + } + + void _updateState() => setState(() {}); + + @override + Widget build(BuildContext context) { + return SliderTheme( + data: const SliderThemeData( + trackHeight: 4, + trackShape: RectangularSliderTrackShape(), + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 0), + overlayShape: VerticalOverlayShape(height: 21), + // overlayShape: SliderComponentShape.noOverlay, + overlayColor: Colors.transparent, + ), + child: Slider( + value: AudioPlayApi.position.inMilliseconds.toDouble(), + max: AudioPlayApi.duration?.inMilliseconds.toDouble() ?? + AudioPlayApi.position.inMilliseconds.toDouble(), + onChanged: (value) => setState(() { + AudioPlayApi.seek(Duration(milliseconds: value.toInt())); + }), + ), + ); + } +} diff --git a/lib/src/component/filtered_tab_bar.dart b/lib/src/component/filtered_tab_bar.dart new file mode 100644 index 0000000..58972cf --- /dev/null +++ b/lib/src/component/filtered_tab_bar.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +class FilteredTabBar extends StatefulWidget { + const FilteredTabBar({ + super.key, + required this.tabTexts, + required this.filters, + required this.controller, + }) : assert(tabTexts.length > 0), + assert(filters.length > 0); + + final List tabTexts; + final List< + ({ + String text, + IconData leadingIcon, + void Function()? onPressed, + })> filters; + final TabController controller; + + @override + State createState() => _FilteredTabBarState(); +} + +class _FilteredTabBarState extends State + with SingleTickerProviderStateMixin { + static const _tabHeight = 40.0; + + late final _controller = widget.controller; + int _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: _tabHeight, + child: Row( + children: [ + Flexible( + child: TabBar( + controller: _controller, + tabs: [ + for (var tabText in widget.tabTexts) + Tab(text: tabText, height: _tabHeight), + ], + overlayColor: WidgetStateProperty.all(Colors.transparent), + dividerColor: Colors.transparent, + ), + ), + MenuAnchor( + style: MenuStyle( + shadowColor: WidgetStateProperty.all(Colors.transparent), + padding: WidgetStateProperty.all(EdgeInsets.zero), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).highlightColor, + width: 2, + ), + borderRadius: BorderRadius.circular(8)), + ), + ), + alignmentOffset: const Offset(-51, 6), + menuChildren: [ + for (var entry in widget.filters.asMap().entries) + Opacity( + opacity: entry.key == _selectedIndex ? 1 : .5, + child: MenuItemButton( + onPressed: () { + setState(() => _selectedIndex = entry.key); + entry.value.onPressed?.call(); + }, + leadingIcon: Icon(entry.value.leadingIcon), + child: Text(entry.value.text), + ), + ), + ], + builder: (_, menuController, __) => IconButton( + visualDensity: VisualDensity.comfortable, + highlightColor: Colors.transparent, + onPressed: () { + if (menuController.isOpen) { + menuController.close(); + } else { + menuController.open(); + } + }, + icon: const Opacity( + opacity: .5, + child: Icon(Icons.filter_alt_outlined), + ), + ), + ), + ], + ), + ), + Divider(height: 0, color: Theme.of(context).dividerColor), + ], + ); + } +} diff --git a/lib/src/component/search_view.dart b/lib/src/component/search_view.dart new file mode 100644 index 0000000..590f1aa --- /dev/null +++ b/lib/src/component/search_view.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +import '../models.dart'; +import '../services.dart'; +import '../utils.dart'; + +class SearchView extends StatefulWidget { + const SearchView({ + super.key, + required this.searchApi, + }); + + final SearchApi searchApi; + + @override + _SearchViewState createState() => _SearchViewState(); +} + +class _SearchViewState extends State { + static const _pageSize = 10; + static const _padding = 12.0; + + final PagingController _pagingController = + PagingController(firstPageKey: 1); + bool _isDisposed = false; + + @override + void initState() { + super.initState(); + _pagingController.addPageRequestListener((pageKey) => _fetchPage(pageKey)); + } + + @override + void dispose() { + _isDisposed = true; + _pagingController.dispose(); + super.dispose(); + } + + Future _fetchPage(int pageKey) async { + try { + final newItems = + await widget.searchApi.getPage(pageKey: pageKey, pageSize: _pageSize); + final isLastPage = newItems.length < _pageSize; + if (_isDisposed) { + return; + } + if (isLastPage) { + _pagingController.appendLastPage(newItems); + } else { + _pagingController.appendPage(newItems, ++pageKey); + } + } catch (error) { + if (_isDisposed) { + return; + } + _pagingController.error = error; + } + } + + @override + Widget build(BuildContext context) => PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + if (!item.isValid) { + return const SizedBox.shrink(); + } + + return Column( + children: [ + InkWell( + onTap: () async { + if (item.isValid) { + await AudioPlayApi.pause(); + await AudioPlayApi.add(item); + await AudioPlayApi.seekTo(item); + AudioPlayApi.play(); + } + }, + onLongPress: () { + if (item.isValid) { + Clipboard.setData( + ClipboardData(text: item.bvid!), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Copied to clipboard'), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(_padding), + child: SearchItem(item: item), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Divider( + height: 0, + color: Theme.of(context).dividerColor, + ), + ), + ], + ); + }, + ), + ); +} + +class SearchItem extends StatelessWidget { + const SearchItem({ + super.key, + required this.item, + }); + + final AudioSummary item; + + @override + Widget build(BuildContext context) { + final previewImage = ClipRRect( + borderRadius: BorderRadius.circular(4), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Image.memory( + item.pic, + fit: BoxFit.cover, + ), + ), + ); + + final durationDisplay = Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(4), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(.6), + borderRadius: BorderRadius.circular(2), + ), + child: Text( + ' ${Display.formatDuration(item.duration)} ', + style: const TextStyle(color: Colors.white, fontSize: 10), + ), + ), + ), + ); + + final titleDisplay = Align( + alignment: Alignment.topLeft, + child: Text( + Display.formatTitle(item.title), + // style: const TextStyle(fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + + final authorDisplay = Row( + children: [ + const Icon( + Icons.person, + size: 18, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + item.author, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ], + ); + + final playAndPubDate = Row( + children: [ + const Icon( + Icons.play_circle_outline, + size: 18, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + '${Display.formatPlay(item.play)} · ${Display.formatPubDate(item.pubDate)}', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ); + + return IntrinsicHeight( + child: Row( + children: [ + Flexible( + flex: 14, + child: Stack( + children: [ + previewImage, + durationDisplay, + ], + ), + ), + const SizedBox(width: 8), + Flexible( + flex: 17, + child: Stack( + children: [ + titleDisplay, + Align( + alignment: Alignment.bottomLeft, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + authorDisplay, + const SizedBox(height: 2), + playAndPubDate, + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/component/user_info.dart b/lib/src/component/user_info.dart new file mode 100644 index 0000000..baa883b --- /dev/null +++ b/lib/src/component/user_info.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import '../models.dart'; + +class UserInfo extends StatelessWidget { + const UserInfo({ + super.key, + required this.userSummary, + }); + + final UserSummary userSummary; + + @override + Widget build(BuildContext context) { + return IntrinsicHeight( + child: Row( + children: [ + CircleAvatar( + backgroundImage: MemoryImage(userSummary.face), + radius: 22.5, + ), + const SizedBox(width: 10), + Stack( + children: [ + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + userSummary.name, + style: TextStyle( + fontSize: 14, + // fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + if (userSummary.tag != null) + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey.shade400 + : Colors.grey.shade600, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + userSummary.tag!, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ) + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/component/user_view.dart b/lib/src/component/user_view.dart new file mode 100644 index 0000000..385615a --- /dev/null +++ b/lib/src/component/user_view.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; + +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +import '../models.dart'; +import '../services.dart'; +import '../utils.dart'; + +class UserView extends StatefulWidget { + const UserView({ + super.key, + required this.userApi, + this.height = 112.5, + }); + + final UserApi userApi; + final double height; + + @override + _UserViewState createState() => _UserViewState(); +} + +class _UserViewState extends State { + static const _pageSize = 5; + static const _padding = 4.0; + + final PagingController _pagingController = + PagingController(firstPageKey: 1); + bool _isDisposed = false; + + @override + void initState() { + super.initState(); + _pagingController.addPageRequestListener((pageKey) => _fetchPage(pageKey)); + } + + @override + void dispose() { + _isDisposed = true; + _pagingController.dispose(); + super.dispose(); + } + + Future _fetchPage(int pageKey) async { + try { + final (audioSummaries: newItems, isLastPage: isLastPage) = + await widget.userApi.getPage(pageKey: pageKey, pageSize: _pageSize); + if (_isDisposed) { + return; + } + if (isLastPage) { + _pagingController.appendLastPage(newItems); + } else { + _pagingController.appendPage(newItems, ++pageKey); + } + } catch (error) { + if (_isDisposed) { + return; + } + _pagingController.error = error; + } + } + + @override + Widget build(BuildContext context) => Flexible( + child: Container( + constraints: BoxConstraints( + maxHeight: widget.height + _padding * 2, + ), + child: PagedListView( + scrollDirection: Axis.horizontal, + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + if (!item.isValid) { + return const SizedBox.shrink(); + } + + return InkWell( + onTap: () async { + if (item.isValid) { + await AudioPlayApi.pause(); + await AudioPlayApi.add(item); + await AudioPlayApi.seekTo(item); + AudioPlayApi.play(); + } + }, + child: Padding( + padding: const EdgeInsets.all(_padding), + child: UserItem( + item: item, + height: widget.height, + ), + ), + ); + }, + newPageProgressIndicatorBuilder: (context) => + const SizedBox.shrink(), + firstPageProgressIndicatorBuilder: (context) => + const SizedBox.shrink(), + ), + ), + ), + ); +} + +class UserItem extends StatefulWidget { + const UserItem({ + super.key, + required this.item, + required this.height, + }); + + static const _circular = 4.0; + static const _textPadding = 6.0; + static const _ratio = 4 / 3; + static const _expandedRatio = 21 / 9; + + final double height; + final AudioSummary item; + + @override + _UserItemState createState() => _UserItemState(); +} + +class _UserItemState extends State { + final _scrollController = ScrollController(); + bool _isLongPressed = false; + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final previewImage = ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(UserItem._circular), + topRight: Radius.circular(UserItem._circular), + ), + child: AspectRatio( + aspectRatio: _isLongPressed ? UserItem._expandedRatio : UserItem._ratio, + child: Image.memory( + widget.item.pic, + fit: BoxFit.cover, + ), + ), + ); + + final durationDisplay = Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(4), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(.6), + borderRadius: BorderRadius.circular(2), + ), + child: Text( + ' ${Display.formatDuration(widget.item.duration)} ', + style: const TextStyle(color: Colors.white, fontSize: 8.5), + ), + ), + ), + ); + + final playDisplay = Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(4), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(.5), + borderRadius: BorderRadius.circular(2), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 1), + const Icon( + Icons.play_arrow_rounded, + size: 12, + color: Colors.white, + ), + Text( + ' ${Display.formatPlay(widget.item.play)} ', + style: const TextStyle(color: Colors.white, fontSize: 8.5), + ), + ], + ), + ), + ), + ); + + return GestureDetector( + onLongPress: () async { + if (!_isLongPressed) { + setState(() => _isLongPressed = true); + if (_scrollController.position.maxScrollExtent > + _scrollController.position.viewportDimension - + UserItem._textPadding * 2) { + await _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 2400), + curve: Curves.easeIn, + ); + } + } + }, + onLongPressUp: () async { + if (_isLongPressed) { + setState(() => _isLongPressed = false); + await _scrollController.animateTo( + _scrollController.position.minScrollExtent, + duration: const Duration(milliseconds: 120), + curve: Curves.easeOut, + ); + } + }, + child: SizedBox( + height: widget.height, + width: _isLongPressed + ? widget.height * (UserItem._expandedRatio / UserItem._ratio) + : widget.height, + child: Stack( + children: [ + SizedBox( + height: widget.height / UserItem._ratio, + child: Stack( + children: [ + previewImage, + durationDisplay, + playDisplay, + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: widget.height * (1 - 1 / UserItem._ratio), + width: _isLongPressed + ? widget.height * + (UserItem._expandedRatio / UserItem._ratio) + : widget.height, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(.9), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(UserItem._circular), + bottomRight: Radius.circular(UserItem._circular), + ), + ), + padding: const EdgeInsets.all(UserItem._textPadding), + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: Text( + Display.formatTitle(widget.item.title, simplify: true), + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/components.dart b/lib/src/components.dart new file mode 100644 index 0000000..0927add --- /dev/null +++ b/lib/src/components.dart @@ -0,0 +1,6 @@ +export 'component/app_search_bar.dart'; +export 'component/filtered_tab_bar.dart'; +export 'component/search_view.dart'; +export 'component/user_view.dart'; +export 'component/user_info.dart'; +export 'component/audio_view.dart'; diff --git a/lib/src/model/audio_summary.dart b/lib/src/model/audio_summary.dart new file mode 100644 index 0000000..9ce0bbd --- /dev/null +++ b/lib/src/model/audio_summary.dart @@ -0,0 +1,103 @@ +import 'package:flutter/foundation.dart'; +import 'package:railgun_minus/src/utils.dart'; + +@immutable +class AudioSummary { + const AudioSummary({ + required this.bvid, + required this.title, + required this.author, + required this.mid, + required this.play, + required this.pubDate, + required this.duration, + required this.pic, + required this.description, + // required this.tags, + required this.like, + required this.favorites, + }); + + AudioSummary.invalid() + : bvid = null, + title = '', + author = '', + mid = 0, + play = 0, + pubDate = DateTime(0), + duration = '', + pic = Uint8List(0), + description = '', + // tags = const [], + like = 0, + favorites = 0; + + factory AudioSummary.fromJson(Map json) => AudioSummary( + bvid: json['bvid'] as String, + title: Format.validTitle(json['title'] as String), + author: json['author'] as String, + mid: json['mid'] as int, + play: json['play'] as int, + pubDate: + DateTime.fromMillisecondsSinceEpoch((json['pubdate'] as int) * 1000) + .toLocal(), + duration: json['duration'] as String, + pic: json['pic'] as Uint8List, + description: json['description'] as String, + // tags: (json['tag'] as String).split(','), + like: json['like'] as int, + favorites: json['favorites'] as int, + ); + + final String? bvid; + final String title; + final String author; + final int mid; + final int play; + final DateTime pubDate; + final String duration; + final Uint8List pic; + final String description; + // final List tags; + final int like; + final int favorites; + + @override + bool operator ==(Object other) { + return other is AudioSummary && + runtimeType == other.runtimeType && + bvid == other.bvid && + title == other.title && + author == other.author && + mid == other.mid && + play == other.play && + pubDate == other.pubDate && + duration == other.duration && + pic == other.pic && + description == other.description && + // tags == other.tags && + like == other.like && + favorites == other.favorites; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + bvid, + title, + author, + mid, + play, + pubDate, + duration, + pic, + description, + // tags, + like, + favorites, + ); + } + + bool get isValid => bvid != null; +} diff --git a/lib/src/model/user_summary.dart b/lib/src/model/user_summary.dart new file mode 100644 index 0000000..a5c8f82 --- /dev/null +++ b/lib/src/model/user_summary.dart @@ -0,0 +1,49 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class UserSummary { + const UserSummary({ + required this.mid, + required this.name, + required this.face, + required this.sign, + this.tag, + }); + + factory UserSummary.fromJson(Map json) => UserSummary( + mid: json['mid'] as int, + name: json['name'] as String, + face: json['face'] as Uint8List, + sign: json['sign'] as String, + tag: json['tag'] as String?, + ); + + final int mid; + final String name; + final Uint8List face; + final String sign; + final String? tag; + + @override + bool operator ==(Object other) { + return other is UserSummary && + runtimeType == other.runtimeType && + mid == other.mid && + name == other.name && + face == other.face && + sign == other.sign && + tag == other.tag; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + mid, + name, + face, + sign, + tag, + ); + } +} diff --git a/lib/src/models.dart b/lib/src/models.dart new file mode 100644 index 0000000..f04681f --- /dev/null +++ b/lib/src/models.dart @@ -0,0 +1,2 @@ +export 'model/audio_summary.dart'; +export 'model/user_summary.dart'; \ No newline at end of file diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart new file mode 100644 index 0000000..051cbbd --- /dev/null +++ b/lib/src/page/home_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:railgun_minus/src/model/user_summary.dart'; + +import '../components.dart'; +import '../services.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final _controller = TextEditingController(); + final _userApis = [ + for (var up in SettingApi.Ups) + if (up.pattern != null) + UserApi( + uid: up.uid, + tag: up.tag, + )..addVerifier( + (jsonData) => RegExp(up.pattern!).hasMatch(jsonData['title']), + ) + else + UserApi( + uid: up.uid, + tag: up.tag, + ) + ]; + final Map _userSummaries = {}; + + @override + void initState() { + super.initState(); + for (var userApi in _userApis) { + userApi.userSummary.then((value) { + setState(() { + _userSummaries[userApi] = value; + }); + }); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppSearchBar( + controller: _controller, + onSubmitted: (value) { + context.go('/search/$value'); + _controller.clear(); + }), + body: Padding( + padding: const EdgeInsets.only(left: 16), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var userApi in _userApis) ...[ + const SizedBox(height: 16), + if (_userSummaries[userApi] != null) ...[ + UserInfo(userSummary: _userSummaries[userApi]!), + const SizedBox(height: 4), + ], + UserView(userApi: userApi), + ], + ], + ), + ), + ), + bottomNavigationBar: const AudioView(), + ); + } +} diff --git a/lib/src/page/search_page.dart b/lib/src/page/search_page.dart new file mode 100644 index 0000000..4b4837e --- /dev/null +++ b/lib/src/page/search_page.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:railgun_minus/src/services.dart'; + +import '../components.dart'; + +class SearchPage extends StatefulWidget { + const SearchPage({ + super.key, + required this.keyword, + }) : assert(keyword != ''); + + final String keyword; + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State + with SingleTickerProviderStateMixin { + static const _tabConfigs = [ + ( + text: '综合', + value: 0, + ), + ( + text: '音乐', + value: 3, + ), + ( + text: '虚拟', + value: 30, + ), + ]; + static const _filterConfigs = [ + ( + text: '默认', + value: 'totalrank', + leadingIcon: Icons.menu, + ), + ( + text: '播放', + value: 'click', + leadingIcon: Icons.play_circle_outline, + ), + ( + text: '时间', + value: 'pubdate', + leadingIcon: Icons.access_time, + ), + ]; + + final _searchController = TextEditingController(); + late final _tabController = + TabController(length: _tabConfigs.length, vsync: this); + + late String _keyword = widget.keyword; + String _order = _filterConfigs[0].value; + + @override + void dispose() { + _searchController.dispose(); + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppSearchBar( + searchText: widget.keyword, + controller: _searchController, + onSubmitted: (value) => setState(() { + _keyword = value; + _order = _filterConfigs[0].value; + }), + ), + body: Column( + children: [ + FilteredTabBar( + key: ValueKey(_keyword), + tabTexts: [for (var tabConfig in _tabConfigs) tabConfig.text], + filters: [ + for (var filterConfig in _filterConfigs) + ( + text: filterConfig.text, + leadingIcon: filterConfig.leadingIcon, + onPressed: () => setState(() => _order = filterConfig.value), + ), + ], + controller: _tabController, + ), + Flexible( + child: TabBarView( + controller: _tabController, + children: [ + for (var tabConfig in _tabConfigs) + SearchView( + key: ValueKey([_keyword, _order]), + searchApi: SearchApi( + keyword: _keyword, + order: _order, + typeId: tabConfig.value, + ), + ), + ], + ), + ), + ], + ), + bottomNavigationBar: const AudioView(), + ); + } +} diff --git a/lib/src/pages.dart b/lib/src/pages.dart new file mode 100644 index 0000000..e8e4864 --- /dev/null +++ b/lib/src/pages.dart @@ -0,0 +1,2 @@ +export 'page/home_page.dart'; +export 'page/search_page.dart'; \ No newline at end of file diff --git a/lib/src/router.dart b/lib/src/router.dart new file mode 100644 index 0000000..b37c7c0 --- /dev/null +++ b/lib/src/router.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'pages.dart'; + +final GoRouter appRouter = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, __) => const SafeArea( + child: HomePage(), + ), + routes: [ + GoRoute( + path: 'search/:keyword', + builder: (_, state) => SafeArea( + child: SearchPage(keyword: state.pathParameters['keyword']!), + ), + ), + ], + ), + ], +); diff --git a/lib/src/service/audio_play_api.dart b/lib/src/service/audio_play_api.dart new file mode 100644 index 0000000..d161f96 --- /dev/null +++ b/lib/src/service/audio_play_api.dart @@ -0,0 +1,322 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; + +import 'package:just_audio/just_audio.dart'; +// import 'package:just_audio_background/just_audio_background.dart'; + +import 'seamless_audio_handler.dart'; +import 'local_lock_caching_audio_source.dart'; +import '../models.dart'; +import 'remote_api.dart'; + +enum PlayMode { + all, + one, + shuffle, +} + +class SummaryLockCachingAudioSource extends LocalLockCachingAudioSource { + final AudioSummary audioSummary; + + SummaryLockCachingAudioSource( + super.uri, { + super.headers, + super.cacheFile, + super.tag, + required this.audioSummary, + }); +} + +class SeamlessAudioPlayer extends AudioPlayer { + @override + Future seekToNext() async { + if (loopMode == LoopMode.one) { + await setLoopMode(LoopMode.all); + await super.seekToNext(); + return await setLoopMode(LoopMode.one); + } + return super.seekToNext(); + } + + @override + Future seekToPrevious() async { + if (loopMode == LoopMode.one) { + await setLoopMode(LoopMode.all); + await super.seekToPrevious(); + return await setLoopMode(LoopMode.one); + } + return super.seekToPrevious(); + } +} + +class AudioPlayApi { + static final AudioPlayApi _instance = AudioPlayApi._(); + + factory AudioPlayApi() => _instance; + + AudioPlayApi._() : _player = SeamlessAudioPlayer(); + + static Future init() async { + await _instance._player.setAudioSource(_instance._audios); + await _instance._player.setShuffleModeEnabled(false); + return await _instance._player.setLoopMode(LoopMode.all); + } + + final AudioPlayer _player; + final ConcatenatingAudioSource _audios = + ConcatenatingAudioSource(children: []); + PlayMode _currentPlayMode = PlayMode.all; + + static AudioPlayer get player => _instance._player; + + static bool get isPlaying => _instance._isPlaying; + bool get _isPlaying => _player.playing; + + static PlayMode get currentPlayMode => _instance._currentPlayMode; + + static List? get audioSummaries => _instance._audioSummaries; + List? get _audioSummaries { + final seq = _player.sequence; + if (seq == null) { + return null; + } + + if (_player.shuffleModeEnabled) { + final indices = _player.shuffleIndices; + if (indices == null) { + return null; + } + + return [ + for (var index in indices) + (seq[index] as SummaryLockCachingAudioSource).audioSummary + ]; + } + + return [ + for (var audio in seq) + (audio as SummaryLockCachingAudioSource).audioSummary + ]; + } + + static int get currentIndex => _instance._currentIndex; + int get _currentIndex { + final realCurrentIndex = _player.currentIndex; + if (realCurrentIndex == null) { + return -1; + } + + if (_player.shuffleModeEnabled) { + final indices = _player.shuffleIndices; + if (indices == null) { + return -1; + } + + return indices.indexOf(realCurrentIndex); + } + + return realCurrentIndex; + } + + static AudioSummary? get currentAudioSummary => + _instance._currentAudioSummary; + AudioSummary? get _currentAudioSummary { + if (currentIndex == -1) { + return null; + } + + final seq = audioSummaries; + if (seq == null || seq.isEmpty) { + return null; + } + + return seq[currentIndex]; + } + + static Duration get position => _instance._position; + Duration get _position => _player.position; + + static Duration? get duration => _instance._duration; + Duration? get _duration => _player.duration; + + static Future add(AudioSummary audioSummary) async => + await _instance._add(audioSummary); + Future _add(AudioSummary audioSummary) async { + if (!audioSummary.isValid) { + return; + } + + if (_audios.children.any((audio) => + (audio as SummaryLockCachingAudioSource).audioSummary.bvid == + audioSummary.bvid)) { + return; + } + + final viewResponse = await RemoteApi.getBaseSrc( + RemoteApiPath.audioInfo, + query: {'bvid': audioSummary.bvid as String}, + ); + final cid = json.decode(viewResponse.body)['data']['cid']; + + final playUrlResponse = await RemoteApi.getBaseSrc( + RemoteApiPath.audioStream, + query: { + 'bvid': audioSummary.bvid as String, + 'cid': cid.toString(), + 'fnval': 16.toString(), + }, + ); + final playUrl = Uri.parse( + (json.decode(playUrlResponse.body)['data']['dash']['audio'] as List) + .last['baseUrl']); + + return await _audios.add( + SummaryLockCachingAudioSource( + playUrl, + headers: RemoteApi.headers, + tag: MediaItem( + id: audioSummary.bvid!, + title: audioSummary.title, + artist: audioSummary.author, + ), + audioSummary: audioSummary, + ), + ); + } + + static Future removeAt(int index) async => _instance._removeAt(index); + Future _removeAt(int index) async { + if (index < 0 || index >= _audios.children.length) { + return; + } + + if (_player.shuffleModeEnabled) { + final indices = _player.shuffleIndices; + if (indices == null) { + return; + } + + final realIndex = indices[index]; + if (realIndex == -1) { + return; + } + + return await _audios.removeAt(realIndex); + } + + return await _audios.removeAt(index); + } + + static Future seek(Duration? position, {int? index}) async => + _instance._seek(position, index: index); + Future _seek(Duration? position, {int? index}) async => + _player.seek(position, index: index); + + static Future seekTo(AudioSummary audioSummary) async => + _instance._seekTo(audioSummary); + Future _seekTo(AudioSummary audioSummary) async { + int index = _player.sequence?.indexWhere((audio) => + (audio as SummaryLockCachingAudioSource).audioSummary.bvid == + audioSummary.bvid) ?? + -1; + + if (index != -1) { + return await _seek(null, index: index); + } + } + + static Future seekToPrevious() async => _instance._seekToPrevious(); + Future _seekToPrevious() async => await _player.seekToPrevious(); + + static Future seekToNext() async => _instance._seekToNext(); + Future _seekToNext() async => await _player.seekToNext(); + + static Future play() async => _instance._play(); + Future _play() async => await _player.play(); + + static Future pause() async => _instance._pause(); + Future _pause() async => await _player.pause(); + + static Future setPlayMode(PlayMode playMode) async => + _instance._setPlayMode(playMode); + Future _setPlayMode(PlayMode playMode) async { + _currentPlayMode = playMode; + switch (playMode) { + case PlayMode.all: + await _player.setLoopMode(LoopMode.all); + await _player.setShuffleModeEnabled(false); + case PlayMode.one: + await _player.setLoopMode(LoopMode.one); + await _player.setShuffleModeEnabled(false); + case PlayMode.shuffle: + await _player.setLoopMode(LoopMode.all); + await _player.setShuffleModeEnabled(true); + await _player.shuffle(); + } + } +} + +class AudioPlayModeNotifier extends ChangeNotifier { + static final AudioPlayModeNotifier _instance = AudioPlayModeNotifier._(); + + factory AudioPlayModeNotifier() => _instance; + + AudioPlayModeNotifier._() { + AudioPlayApi.player.loopModeStream.listen((loopMode) => notifyListeners()); + AudioPlayApi.player.shuffleModeEnabledStream + .listen((shuffleMode) => notifyListeners()); + } + + static void registerListener(VoidCallback listener) => + _instance.addListener(listener); + static void unregisterListener(VoidCallback listener) => + _instance.removeListener(listener); +} + +class AudioPositionNotifier extends ChangeNotifier { + static final AudioPositionNotifier _instance = AudioPositionNotifier._(); + + factory AudioPositionNotifier() => _instance; + + AudioPositionNotifier._() { + AudioPlayApi.player.positionStream.listen((position) => notifyListeners()); + } + + static void registerListener(VoidCallback listener) => + _instance.addListener(listener); + static void unregisterListener(VoidCallback listener) => + _instance.removeListener(listener); +} + +class AudioSequenceNotifier extends ChangeNotifier { + static final AudioSequenceNotifier _instance = AudioSequenceNotifier._(); + + factory AudioSequenceNotifier() => _instance; + + AudioSequenceNotifier._() { + AudioPlayApi.player.sequenceStream.listen((sequence) => notifyListeners()); + AudioPlayApi.player.shuffleIndicesStream + .listen((indices) => notifyListeners()); + AudioPlayApi.player.currentIndexStream.listen((index) => notifyListeners()); + } + + static void registerListener(VoidCallback listener) => + _instance.addListener(listener); + static void unregisterListener(VoidCallback listener) => + _instance.removeListener(listener); +} + +class AudioPlayingNotifier extends ChangeNotifier { + static final AudioPlayingNotifier _instance = AudioPlayingNotifier._(); + + factory AudioPlayingNotifier() => _instance; + + AudioPlayingNotifier._() { + AudioPlayApi.player.playingStream.listen((playing) => notifyListeners()); + } + + static void registerListener(VoidCallback listener) => + _instance.addListener(listener); + static void unregisterListener(VoidCallback listener) => + _instance.removeListener(listener); +} diff --git a/lib/src/service/local_lock_caching_audio_source.dart b/lib/src/service/local_lock_caching_audio_source.dart new file mode 100644 index 0000000..a385a2b --- /dev/null +++ b/lib/src/service/local_lock_caching_audio_source.dart @@ -0,0 +1,447 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:just_audio/just_audio.dart'; + +Future _getCacheDir() async => Directory( + p.join((await getApplicationCacheDirectory()).path, 'just_audio_cache')); + +HttpClient _createHttpClient({String? userAgent}) { + final client = HttpClient(); + if (userAgent != null) { + client.userAgent = userAgent; + } + return client; +} + +Future _getUrl(HttpClient client, Uri uri, + {Map? headers}) async { + final request = await client.getUrl(uri); + if (headers != null) { + final host = request.headers.value(HttpHeaders.hostHeader); + request.headers.clear(); + request.headers.set(HttpHeaders.contentLengthHeader, '0'); + headers.forEach((name, value) => request.headers.set(name, value)); + if (host != null) { + request.headers.set(HttpHeaders.hostHeader, host); + } + if (client.userAgent != null) { + request.headers.set(HttpHeaders.userAgentHeader, client.userAgent!); + } + } + // Match ExoPlayer's native behavior + request.maxRedirects = 20; + return request; +} + +/// Encapsulates the start and end of an HTTP range request. +class _HttpRangeRequest { + /// The starting byte position of the range request. + final int start; + + /// The last byte position of the range request, or `null` if requesting + /// until the end of the media. + final int? end; + + /// The end byte position (exclusive), defaulting to `null`. + int? get endEx => end == null ? null : end! + 1; + + _HttpRangeRequest(this.start, this.end); + + /// Format a range header for this request. + String get header => + 'bytes=$start-${end != null ? (end! - 1).toString() : ""}'; + + /// Creates an [_HttpRangeRequest] from [header]. + static _HttpRangeRequest? parse(List? header) { + if (header == null || header.isEmpty) return null; + final match = RegExp(r'^bytes=(\d+)(-(\d+)?)?').firstMatch(header.first); + if (match == null) return null; + int? intGroup(int i) => match[i] != null ? int.parse(match[i]!) : null; + return _HttpRangeRequest(intGroup(1)!, intGroup(3)); + } +} + +/// Request parameters for a [StreamAudioSource]. +class _StreamingByteRangeRequest { + /// The start of the range request. + final int? start; + + /// The end of the range request. + final int? end; + + /// Completes when the response is available. + final _completer = Completer(); + + _StreamingByteRangeRequest(this.start, this.end); + + /// The response for this request. + Future get future => _completer.future; + + /// Completes this request with the given [response]. + void complete(StreamAudioResponse response) { + if (_completer.isCompleted) { + return; + } + _completer.complete(response); + } + + /// Fails this request with the given [error] and [stackTrace]. + void fail(dynamic error, [StackTrace? stackTrace]) { + if (_completer.isCompleted) { + return; + } + _completer.completeError(error as Object, stackTrace); + } +} + +/// When a byte range request on a [LockCachingAudioSource] overlaps partially +/// with the cache file and partially with the live HTTP stream, the consumer +/// needs to first consume the cached part before the live part. This class +/// provides a place to buffer the live part until the consumer reaches it, and +/// also keeps track of the [end] of the byte range so that the producer knows +/// when to stop adding data. +class _InProgressCacheResponse { + // NOTE: This isn't necessarily memory efficient. Since the entire audio file + // will likely be downloaded at a faster rate than the rate at which the + // player is consuming audio data, it is also likely that this buffered data + // will never be used. + // TODO: Improve this code. + // ignore: close_sinks + final controller = ReplaySubject>(); + final int? end; + _InProgressCacheResponse({ + required this.end, + }); +} + +/// This is an experimental audio source that caches the audio while it is being +/// downloaded and played. It is not supported on platforms that do not provide +/// access to the file system (e.g. web). +class LocalLockCachingAudioSource extends StreamAudioSource { + Future? _response; + final Uri uri; + final Map? headers; + final Future cacheFile; + int _progress = 0; + final _requests = <_StreamingByteRangeRequest>[]; + final _downloadProgressSubject = BehaviorSubject(); + bool _downloading = false; + + /// Creates a [LocalLockCachingAudioSource] to that provides [uri] to the player + /// while simultaneously caching it to [cacheFile]. If no cache file is + /// supplied, just_audio will allocate a cache file internally. + /// + /// If headers are set, just_audio will create a cleartext local HTTP proxy on + /// your device to forward HTTP requests with headers included. + LocalLockCachingAudioSource( + this.uri, { + this.headers, + File? cacheFile, + super.tag, + }) : cacheFile = + cacheFile != null ? Future.value(cacheFile) : _getCacheFile(uri) { + _init(); + } + + Future _init() async { + final cacheFile = await this.cacheFile; + _downloadProgressSubject.add((await cacheFile.exists()) ? 1.0 : 0.0); + } + + /// Returns a [UriAudioSource] resolving directly to the cache file if it + /// exists, otherwise returns `this`. This can be + Future resolve() async { + final file = await cacheFile; + return await file.exists() ? AudioSource.uri(Uri.file(file.path)) : this; + } + + /// Emits the current download progress as a double value from 0.0 (nothing + /// downloaded) to 1.0 (download complete). + Stream get downloadProgressStream => _downloadProgressSubject.stream; + + /// Removes the underlying cache files. It is an error to clear the cache + /// while a download is in progress. + Future clearCache() async { + if (_downloading) { + throw Exception("Cannot clear cache while download is in progress"); + } + _response = null; + final cacheFile = await this.cacheFile; + if (await cacheFile.exists()) { + await cacheFile.delete(); + } + final mimeFile = await _mimeFile; + if (await mimeFile.exists()) { + await mimeFile.delete(); + } + _progress = 0; + _downloadProgressSubject.add(0.0); + } + + /// Get file for caching [uri] with proper extension + static Future _getCacheFile(final Uri uri) async => File(p.joinAll([ + (await _getCacheDir()).path, + 'remote', + sha256 + .convert(utf8.encode(uri.pathSegments.last.toString())) + .toString() + + p.extension(uri.path), + ])); + + Future get _partialCacheFile async => + File('${(await cacheFile).path}.part'); + + /// We use this to record the original content type of the downloaded audio. + /// NOTE: We could instead rely on the cache file extension, but the original + /// URL might not provide a correct extension. As a fallback, we could map the + /// MIME type to an extension but we will need a complete dictionary. + Future get _mimeFile async => File('${(await cacheFile).path}.mime'); + + Future _readCachedMimeType() async { + final file = await _mimeFile; + if (file.existsSync()) { + return (await _mimeFile).readAsString(); + } else { + return 'audio/mpeg'; + } + } + + /// Start downloading the whole audio file to the cache and fulfill byte-range + /// requests during the download. There are 3 scenarios: + /// + /// 1. If the byte range request falls entirely within the cache region, it is + /// fulfilled from the cache. + /// 2. If the byte range request overlaps the cached region, the first part is + /// fulfilled from the cache, and the region beyond the cache is fulfilled + /// from a memory buffer of the downloaded data. + /// 3. If the byte range request is entirely outside the cached region, a + /// separate HTTP request is made to fulfill it while the download of the + /// entire file continues in parallel. + Future _fetch() async { + _downloading = true; + final cacheFile = await this.cacheFile; + final partialCacheFile = await _partialCacheFile; + + File getEffectiveCacheFile() => + partialCacheFile.existsSync() ? partialCacheFile : cacheFile; + + final httpClient = _createHttpClient(); + // final httpClient = _createHttpClient(userAgent: _player?._userAgent); + final httpRequest = await _getUrl(httpClient, uri, headers: headers); + final response = await httpRequest.close(); + if (response.statusCode != 200) { + httpClient.close(); + throw Exception('HTTP Status Error: ${response.statusCode}'); + } + (await _partialCacheFile).createSync(recursive: true); + // TODO: Should close sink after done, but it throws an error. + // ignore: close_sinks + final sink = (await _partialCacheFile).openWrite(); + final sourceLength = + response.contentLength == -1 ? null : response.contentLength; + final mimeType = response.headers.contentType.toString(); + final acceptRanges = response.headers.value(HttpHeaders.acceptRangesHeader); + final originSupportsRangeRequests = + acceptRanges != null && acceptRanges != 'none'; + final mimeFile = await _mimeFile; + await mimeFile.writeAsString(mimeType); + final inProgressResponses = <_InProgressCacheResponse>[]; + late StreamSubscription> subscription; + var percentProgress = 0; + void updateProgress(int newPercentProgress) { + if (newPercentProgress != percentProgress) { + percentProgress = newPercentProgress; + _downloadProgressSubject.add(percentProgress / 100); + } + } + + _progress = 0; + subscription = response.listen((data) async { + _progress += data.length; + final newPercentProgress = (sourceLength == null) + ? 0 + : (sourceLength == 0) + ? 100 + : (100 * _progress ~/ sourceLength); + updateProgress(newPercentProgress); + sink.add(data); + final readyRequests = _requests + .where((request) => + !originSupportsRangeRequests || + request.start == null || + (request.start!) < _progress) + .toList(); + final notReadyRequests = _requests + .where((request) => + originSupportsRangeRequests && + request.start != null && + (request.start!) >= _progress) + .toList(); + // Add this live data to any responses in progress. + for (var cacheResponse in inProgressResponses) { + final end = cacheResponse.end; + if (end != null && _progress >= end) { + // We've received enough data to fulfill the byte range request. + final subEnd = + min(data.length, max(0, data.length - (_progress - end))); + cacheResponse.controller.add(data.sublist(0, subEnd)); + cacheResponse.controller.close(); + } else { + cacheResponse.controller.add(data); + } + } + inProgressResponses.removeWhere((element) => element.controller.isClosed); + if (_requests.isEmpty) return; + // Prevent further data coming from the HTTP source until we have set up + // an entry in inProgressResponses to continue receiving live HTTP data. + subscription.pause(); + await sink.flush(); + // Process any requests that start within the cache. + for (var request in readyRequests) { + _requests.remove(request); + int? start, end; + if (originSupportsRangeRequests) { + start = request.start; + end = request.end; + } else { + // If the origin doesn't support range requests, the proxy should also + // ignore range requests and instead serve a complete 200 response + // which the client (AV or exo player) should know how to deal with. + } + final effectiveStart = start ?? 0; + final effectiveEnd = end ?? sourceLength; + Stream> responseStream; + if (effectiveEnd != null && effectiveEnd <= _progress) { + responseStream = + getEffectiveCacheFile().openRead(effectiveStart, effectiveEnd); + } else { + final cacheResponse = _InProgressCacheResponse(end: effectiveEnd); + inProgressResponses.add(cacheResponse); + responseStream = Rx.concatEager([ + // NOTE: The cache file part of the stream must not overlap with + // the live part. "_progress" should + // to the cache file at the time + getEffectiveCacheFile().openRead(effectiveStart, _progress), + cacheResponse.controller.stream, + ]); + } + request.complete(StreamAudioResponse( + rangeRequestsSupported: originSupportsRangeRequests, + sourceLength: start != null ? sourceLength : null, + contentLength: + effectiveEnd != null ? effectiveEnd - effectiveStart : null, + offset: start, + contentType: mimeType, + stream: responseStream.asBroadcastStream(), + )); + } + subscription.resume(); + // Process any requests that start beyond the cache. + for (var request in notReadyRequests) { + _requests.remove(request); + final start = request.start!; + final end = request.end ?? sourceLength; + final httpClient = _createHttpClient(); + // final httpClient = _createHttpClient(userAgent: _player?._userAgent); + + final rangeRequest = _HttpRangeRequest(start, end); + _getUrl(httpClient, uri, headers: { + if (headers != null) ...headers!, + HttpHeaders.rangeHeader: rangeRequest.header, + }).then((httpRequest) async { + final response = await httpRequest.close(); + if (response.statusCode != 206) { + httpClient.close(); + throw Exception('HTTP Status Error: ${response.statusCode}'); + } + request.complete(StreamAudioResponse( + rangeRequestsSupported: originSupportsRangeRequests, + sourceLength: sourceLength, + contentLength: end != null ? end - start : null, + offset: start, + contentType: mimeType, + stream: response.asBroadcastStream(), + )); + }, onError: (dynamic e, StackTrace? stackTrace) { + request.fail(e, stackTrace); + }).onError((Object e, StackTrace st) { + request.fail(e, st); + }); + } + }, onDone: () async { + if (sourceLength == null) { + updateProgress(100); + } + for (var cacheResponse in inProgressResponses) { + if (!cacheResponse.controller.isClosed) { + cacheResponse.controller.close(); + } + } + (await _partialCacheFile).renameSync(cacheFile.path); + await subscription.cancel(); + httpClient.close(); + _downloading = false; + }, onError: (Object e, StackTrace stackTrace) async { + (await _partialCacheFile).deleteSync(); + httpClient.close(); + // Fail all pending requests + for (final req in _requests) { + req.fail(e, stackTrace); + } + _requests.clear(); + // Close all in progress requests + for (final res in inProgressResponses) { + res.controller.addError(e, stackTrace); + res.controller.close(); + } + _downloading = false; + }, cancelOnError: true); + return response; + } + + @override + Future request([int? start, int? end]) async { + final cacheFile = await this.cacheFile; + if (cacheFile.existsSync()) { + final sourceLength = cacheFile.lengthSync(); + return StreamAudioResponse( + rangeRequestsSupported: true, + sourceLength: start != null ? sourceLength : null, + contentLength: (end ?? sourceLength) - (start ?? 0), + offset: start, + contentType: await _readCachedMimeType(), + stream: cacheFile.openRead(start, end).asBroadcastStream(), + ); + } + final byteRangeRequest = _StreamingByteRangeRequest(start, end); + _requests.add(byteRangeRequest); + _response ??= + _fetch().catchError((dynamic error, StackTrace? stackTrace) async { + // So that we can restart later + _response = null; + // Cancel any pending request + for (final req in _requests) { + req.fail(error, stackTrace); + } + return Future.error(error as Object, stackTrace); + }); + return byteRangeRequest.future.then((response) { + response.stream.listen((event) {}, onError: (Object e, StackTrace st) { + // So that we can restart later + _response = null; + // Cancel any pending request + for (final req in _requests) { + req.fail(e, st); + } + }); + return response; + }); + } +} diff --git a/lib/src/service/remote_api.dart b/lib/src/service/remote_api.dart new file mode 100644 index 0000000..9832b12 --- /dev/null +++ b/lib/src/service/remote_api.dart @@ -0,0 +1,197 @@ +// import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:cronet_http/cronet_http.dart'; +// import 'package:crypto/crypto.dart'; + +enum RemoteApiPath { + // wbi, + search, + audioInfo, + audioStream, + userInfo, + userSpace, +} + +class RemoteApi { + static final RemoteApi _instance = RemoteApi._(); + + factory RemoteApi() => _instance; + + RemoteApi._() + : _client = CronetClient.fromCronetEngine( + CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: 32 * 1024 * 1024, + ), + closeEngine: true, + ); + + static Future init() async { + final response = + await _instance._client.get(Uri.parse(_headers['Referer']!)); + _headers['Cookie'] = response.headers['set-cookie']!; + // _instance._client.get(getBaseUri(RemoteApiPath.wbi)).then((response) { + // final responseJson = json.decode(response.body); + + // final imgKey = (responseJson['data']['wbi_img']['img_url'] as String) + // .split('/') + // .last + // .split('.') + // .first; + // final subKey = (responseJson['data']['wbi_img']['sub_url'] as String) + // .split('/') + // .last + // .split('.') + // .first; + // final rawWbiKey = imgKey + subKey; + + // _wbiKey = mixinKeyEncryptionTable + // .map((i) => rawWbiKey[i]) + // .join() + // .substring(0, 32); + // }); + } + + static const base = 'api.bilibili.com'; + static const accountBase = 'api.vc.bilibili.com'; + static final Map _headers = { + 'User-Agent': 'Mozilla/5.0', + 'Referer': 'https://www.bilibili.com', + }; + // static const mixinKeyEncryptionTable = [ + // 46, + // 47, + // 18, + // 2, + // 53, + // 8, + // 23, + // 32, + // 15, + // 50, + // 10, + // 31, + // 58, + // 3, + // 45, + // 35, + // 27, + // 43, + // 5, + // 49, + // 33, + // 9, + // 42, + // 19, + // 29, + // 28, + // 14, + // 39, + // 12, + // 38, + // 41, + // 13, + // 37, + // 48, + // 7, + // 16, + // 24, + // 55, + // 40, + // 61, + // 26, + // 17, + // 0, + // 1, + // 60, + // 51, + // 30, + // 4, + // 22, + // 25, + // 54, + // 21, + // 56, + // 59, + // 6, + // 63, + // 57, + // 62, + // 11, + // 36, + // 20, + // 34, + // 44, + // 52 + // ]; + // static late final String _wbiKey; + final http.Client _client; + + static Map get headers => _headers; + static String getPath(RemoteApiPath path) => switch (path) { + // RemoteApiPath.wbi => 'x/web-interface/nav', + RemoteApiPath.search => 'x/web-interface/search/type', + RemoteApiPath.audioInfo => 'x/web-interface/view', + RemoteApiPath.audioStream => 'x/player/playurl', + RemoteApiPath.userInfo => 'account/v1/user/cards', + RemoteApiPath.userSpace => 'x/series/recArchivesByKeywords', + }; + static Uri getBaseUri( + RemoteApiPath path, { + Map? query, + }) { + final apiBase = path == RemoteApiPath.userInfo ? accountBase : base; + return Uri.https(apiBase, getPath(path), query); + } + + static Future get( + Uri url, { + Map? headers, + }) async => + _instance._get(url, headers: headers); + + Future _get( + Uri url, { + Map? headers, + }) async => + await _client.get(url, headers: {..._headers, ...?headers}); + + static Future getBaseSrc( + RemoteApiPath path, { + Map? query, + Map? headers, + }) async => + await get(getBaseUri(path, query: query), headers: headers); + + // static Map encryptWbi(Map? query) => + // _instance._encryptWbi(query); + // Map _encryptWbi(Map? query) { + // query ??= {}; + + // query['wts'] = + // (DateTime.now().millisecondsSinceEpoch / 1000).round().toString(); + + // query = Map.fromEntries( + // query.entries.toList() + // ..sort( + // (a, b) => a.key.compareTo(b.key), + // ), + // ).map( + // (k, v) => MapEntry( + // k, + // v.replaceAll(RegExp(r"[!'()*]"), ''), + // ), + // ); + + // query['w_rid'] = md5 + // .convert( + // utf8.encode( + // Uri(queryParameters: query).query + _wbiKey, + // ), + // ) + // .toString(); + + // return query; + // } +} diff --git a/lib/src/service/seamless_audio_handler.dart b/lib/src/service/seamless_audio_handler.dart new file mode 100644 index 0000000..8afa5cb --- /dev/null +++ b/lib/src/service/seamless_audio_handler.dart @@ -0,0 +1,963 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:just_audio_platform_interface/just_audio_platform_interface.dart'; +import 'package:rxdart/rxdart.dart'; + +export 'package:audio_service/audio_service.dart' show MediaItem; + +late SwitchAudioHandler _audioHandler; +late JustAudioPlatform _platform; + +/// Provides the [init] method to initialise just_audio for background playback. +class SeamlessJustAudioBackground { + /// Initialise just_audio for background playback. This should be called from + /// your app's `main` method. e.g.: + /// + /// ```dart + /// Future main() async { + /// await JustAudioBackground.init( + /// androidNotificationChannelId: 'com.ryanheise.bg_demo.channel.audio', + /// androidNotificationChannelName: 'Audio playback', + /// androidNotificationOngoing: true, + /// ); + /// runApp(MyApp()); + /// } + /// ``` + /// + /// Each parameter controls a behaviour in audio_service. Consult + /// audio_service's `AudioServiceConfig` API documentation for more + /// information. + static Future init({ + bool androidResumeOnClick = true, + String? androidNotificationChannelId, + String androidNotificationChannelName = 'Notifications', + String? androidNotificationChannelDescription, + Color? notificationColor, + String androidNotificationIcon = 'mipmap/ic_launcher', + bool androidShowNotificationBadge = false, + bool androidNotificationClickStartsActivity = true, + bool androidNotificationOngoing = false, + bool androidStopForegroundOnPause = true, + int? artDownscaleWidth, + int? artDownscaleHeight, + Duration fastForwardInterval = const Duration(seconds: 10), + Duration rewindInterval = const Duration(seconds: 10), + bool preloadArtwork = false, + Map? androidBrowsableRootExtras, + }) async { + WidgetsFlutterBinding.ensureInitialized(); + await _SeamlessJustAudioBackgroundPlugin.setup( + androidResumeOnClick: androidResumeOnClick, + androidNotificationChannelId: androidNotificationChannelId, + androidNotificationChannelName: androidNotificationChannelName, + androidNotificationChannelDescription: + androidNotificationChannelDescription, + notificationColor: notificationColor, + androidNotificationIcon: androidNotificationIcon, + androidShowNotificationBadge: androidShowNotificationBadge, + androidNotificationClickStartsActivity: + androidNotificationClickStartsActivity, + androidNotificationOngoing: androidNotificationOngoing, + androidStopForegroundOnPause: androidStopForegroundOnPause, + artDownscaleWidth: artDownscaleWidth, + artDownscaleHeight: artDownscaleHeight, + fastForwardInterval: fastForwardInterval, + rewindInterval: rewindInterval, + preloadArtwork: preloadArtwork, + androidBrowsableRootExtras: androidBrowsableRootExtras, + ); + } +} + +class _SeamlessJustAudioBackgroundPlugin extends JustAudioPlatform { + static Future setup({ + bool androidResumeOnClick = true, + String? androidNotificationChannelId, + String androidNotificationChannelName = 'Notifications', + String? androidNotificationChannelDescription, + Color? notificationColor, + String androidNotificationIcon = 'mipmap/ic_launcher', + bool androidShowNotificationBadge = false, + bool androidNotificationClickStartsActivity = true, + bool androidNotificationOngoing = false, + bool androidStopForegroundOnPause = true, + int? artDownscaleWidth, + int? artDownscaleHeight, + Duration fastForwardInterval = const Duration(seconds: 10), + Duration rewindInterval = const Duration(seconds: 10), + bool preloadArtwork = false, + Map? androidBrowsableRootExtras, + }) async { + _platform = JustAudioPlatform.instance; + JustAudioPlatform.instance = _SeamlessJustAudioBackgroundPlugin(); + _audioHandler = await AudioService.init( + builder: () => SwitchAudioHandler(BaseAudioHandler()), + config: AudioServiceConfig( + androidResumeOnClick: androidResumeOnClick, + androidNotificationChannelId: androidNotificationChannelId, + androidNotificationChannelName: androidNotificationChannelName, + androidNotificationChannelDescription: + androidNotificationChannelDescription, + notificationColor: notificationColor, + androidNotificationIcon: androidNotificationIcon, + androidShowNotificationBadge: androidShowNotificationBadge, + androidNotificationClickStartsActivity: + androidNotificationClickStartsActivity, + androidNotificationOngoing: androidNotificationOngoing, + androidStopForegroundOnPause: androidStopForegroundOnPause, + artDownscaleWidth: artDownscaleWidth, + artDownscaleHeight: artDownscaleHeight, + fastForwardInterval: fastForwardInterval, + rewindInterval: rewindInterval, + preloadArtwork: preloadArtwork, + androidBrowsableRootExtras: androidBrowsableRootExtras, + ), + ); + } + + _SeamlessJustAudioPlayer? _player; + + _SeamlessJustAudioBackgroundPlugin(); + + @override + Future init(InitRequest request) async { + if (_player != null) { + throw PlatformException( + code: "error", + message: + "just_audio_background supports only a single player instance"); + } + _player = _SeamlessJustAudioPlayer( + initRequest: request, + ); + return _player!; + } + + @override + Future disposePlayer( + DisposePlayerRequest request) async { + final player = _player; + _player = null; + await player?.release(); + return DisposePlayerResponse(); + } + + @override + Future disposeAllPlayers( + DisposeAllPlayersRequest request) async { + final player = _player; + _player = null; + await player?.release(); + return DisposeAllPlayersResponse(); + } +} + +class _SeamlessJustAudioPlayer extends AudioPlayerPlatform { + final InitRequest initRequest; + final eventController = StreamController.broadcast(); + final playerDataController = StreamController.broadcast(); + bool? _playing; + IcyMetadataMessage? _icyMetadata; + int? _androidAudioSessionId; + late final _SeamlessPlayerAudioHandler _playerAudioHandler; + + _SeamlessJustAudioPlayer({required this.initRequest}) + : super(initRequest.id) { + _playerAudioHandler = _SeamlessPlayerAudioHandler(initRequest); + _audioHandler.inner = _playerAudioHandler; + _audioHandler.playbackState.listen((playbackState) { + broadcastPlaybackEvent(); + }); + _audioHandler.customEvent.listen((dynamic event) { + switch (event['type']) { + case 'icyMetadata': + _icyMetadata = event['value'] as IcyMetadataMessage?; + broadcastPlaybackEvent(); + break; + case 'androidAudioSessionId': + _androidAudioSessionId = event['value'] as int?; + broadcastPlaybackEvent(); + break; + } + }); + _audioHandler.mediaItem.listen((mediaItem) { + if (mediaItem == null) return; + broadcastPlaybackEvent(); + }); + } + + PlaybackState get playbackState => _audioHandler.playbackState.nvalue!; + + Future release() async { + eventController.close(); + await _audioHandler.stop(); + } + + void broadcastPlaybackEvent() { + if (eventController.isClosed) return; + eventController.add(PlaybackEventMessage( + //processingState: playbackState.processingState, + processingState: { + AudioProcessingState.idle: ProcessingStateMessage.idle, + AudioProcessingState.loading: ProcessingStateMessage.loading, + AudioProcessingState.ready: ProcessingStateMessage.ready, + AudioProcessingState.buffering: ProcessingStateMessage.buffering, + AudioProcessingState.completed: ProcessingStateMessage.completed, + AudioProcessingState.error: ProcessingStateMessage.idle, + }[playbackState.processingState]!, + updatePosition: playbackState.position, + updateTime: playbackState.updateTime, + bufferedPosition: playbackState.bufferedPosition, + icyMetadata: _icyMetadata, + duration: _playerAudioHandler.currentMediaItem?.duration, + currentIndex: playbackState.queueIndex, + androidAudioSessionId: _androidAudioSessionId, + )); + if (playbackState.playing != _playing) { + _playing = playbackState.playing; + playerDataController.add(PlayerDataMessage( + playing: playbackState.playing, + )); + } + } + + @override + Stream get playbackEventMessageStream => + eventController.stream; + + @override + Stream get playerDataMessageStream => + playerDataController.stream; + + @override + Future load(LoadRequest request) => + _playerAudioHandler.customLoad(request); + + @override + Future play(PlayRequest request) async { + await _audioHandler.play(); + return PlayResponse(); + } + + @override + Future pause(PauseRequest request) async { + await _audioHandler.pause(); + return PauseResponse(); + } + + @override + Future setVolume(SetVolumeRequest request) => + _playerAudioHandler.customSetVolume(request); + + @override + Future setSpeed(SetSpeedRequest request) async { + await _playerAudioHandler.setSpeed(request.speed); + return SetSpeedResponse(); + } + + @override + Future setPitch(SetPitchRequest request) async { + await _playerAudioHandler.customSetPitch(request); + return SetPitchResponse(); + } + + @override + Future setSkipSilence( + SetSkipSilenceRequest request) async { + await _playerAudioHandler.customSetSkipSilence(request); + return SetSkipSilenceResponse(); + } + + @override + Future setLoopMode(SetLoopModeRequest request) async { + await _audioHandler + .setRepeatMode(AudioServiceRepeatMode.values[request.loopMode.index]); + return SetLoopModeResponse(); + } + + @override + Future setShuffleMode( + SetShuffleModeRequest request) async { + await _audioHandler.setShuffleMode( + AudioServiceShuffleMode.values[request.shuffleMode.index]); + return SetShuffleModeResponse(); + } + + @override + Future setShuffleOrder( + SetShuffleOrderRequest request) => + _playerAudioHandler.customSetShuffleOrder(request); + + @override + Future setWebCrossOrigin( + SetWebCrossOriginRequest request) async { + _playerAudioHandler.customSetWebCrossOrigin(request); + return SetWebCrossOriginResponse(); + } + + @override + Future seek(SeekRequest request) => + _playerAudioHandler.customPlayerSeek(request); + + @override + Future concatenatingInsertAll( + ConcatenatingInsertAllRequest request) => + _playerAudioHandler.customConcatenatingInsertAll(request); + + @override + Future concatenatingRemoveRange( + ConcatenatingRemoveRangeRequest request) => + _playerAudioHandler.customConcatenatingRemoveRange(request); + + @override + Future concatenatingMove( + ConcatenatingMoveRequest request) => + _playerAudioHandler.customConcatenatingMove(request); + + @override + Future setAndroidAudioAttributes( + SetAndroidAudioAttributesRequest request) => + _playerAudioHandler.customSetAndroidAudioAttributes(request); + + @override + Future + setAutomaticallyWaitsToMinimizeStalling( + SetAutomaticallyWaitsToMinimizeStallingRequest request) => + _playerAudioHandler + .customSetAutomaticallyWaitsToMinimizeStalling(request); + + @override + Future androidEqualizerBandSetGain( + AndroidEqualizerBandSetGainRequest request) => + _playerAudioHandler.customAndroidEqualizerBandSetGain(request); + + @override + Future androidEqualizerGetParameters( + AndroidEqualizerGetParametersRequest request) => + _playerAudioHandler.customAndroidEqualizerGetParameters(request); + + @override + Future + androidLoudnessEnhancerSetTargetGain( + AndroidLoudnessEnhancerSetTargetGainRequest request) => + _playerAudioHandler + .customAndroidLoudnessEnhancerSetTargetGain(request); + + @override + Future audioEffectSetEnabled( + AudioEffectSetEnabledRequest request) => + _playerAudioHandler.customAudioEffectSetEnabled(request); + + @override + Future setAllowsExternalPlayback( + SetAllowsExternalPlaybackRequest request) => + _playerAudioHandler.customSetAllowsExternalPlayback(request); + + @override + Future + setCanUseNetworkResourcesForLiveStreamingWhilePaused( + SetCanUseNetworkResourcesForLiveStreamingWhilePausedRequest + request) => + _playerAudioHandler + .customSetCanUseNetworkResourcesForLiveStreamingWhilePaused( + request); + + @override + Future setPreferredPeakBitRate( + SetPreferredPeakBitRateRequest request) => + _playerAudioHandler.customSetPreferredPeakBitRate(request); +} + +class _SeamlessPlayerAudioHandler extends BaseAudioHandler + with QueueHandler, SeekHandler { + final _playerCompleter = Completer(); + PlaybackEventMessage _justAudioEvent = PlaybackEventMessage( + processingState: ProcessingStateMessage.idle, + updateTime: DateTime.now(), + updatePosition: Duration.zero, + bufferedPosition: Duration.zero, + duration: null, + icyMetadata: null, + currentIndex: null, + androidAudioSessionId: null, + ); + AudioSourceMessage? _source; + bool _playing = false; + double _speed = 1.0; + _SeamlessSeeker? _seeker; + AudioServiceRepeatMode _repeatMode = AudioServiceRepeatMode.none; + AudioServiceShuffleMode _shuffleMode = AudioServiceShuffleMode.none; + List _shuffleIndices = []; + List _shuffleIndicesInv = []; + List _effectiveIndices = []; + List _effectiveIndicesInv = []; + + Future get _player => _playerCompleter.future; + int? get index => _justAudioEvent.currentIndex; + MediaItem? get currentMediaItem => index != null && + currentQueue != null && + index! >= 0 && + index! < currentQueue!.length + ? currentQueue![index!] + : null; + + List? get currentQueue => queue.nvalue; + + _SeamlessPlayerAudioHandler(InitRequest initRequest) { + _init(initRequest); + } + + Future _init(InitRequest initRequest) async { + final player = await _platform.init(initRequest); + _playerCompleter.complete(player); + final playbackEventMessageStream = player.playbackEventMessageStream; + playbackEventMessageStream.listen((event) { + _justAudioEvent = event; + _broadcastState(); + }); + playbackEventMessageStream + .map((event) => event.icyMetadata) + .distinct() + .listen((icyMetadata) { + customEvent.add({ + 'type': 'icyMetadata', + 'value': icyMetadata, + }); + }); + playbackEventMessageStream + .map((event) => event.androidAudioSessionId) + .distinct() + .listen((audioSessionId) { + customEvent.add({ + 'type': 'androidAudioSessionId', + 'value': audioSessionId, + }); + }); + playbackEventMessageStream + .map((event) => SeamlessTrackInfo(event.currentIndex, event.duration)) + .distinct() + .debounceTime(const Duration(milliseconds: 100)) + .map((track) { + // Platform may send us a null duration on dispose, which we should + // ignore. + final currentMediaItem = this.currentMediaItem; + if (currentMediaItem != null) { + if (track.duration == null && currentMediaItem.duration != null) { + return SeamlessTrackInfo(track.index, currentMediaItem.duration); + } + } + return track; + }) + .distinct() + .listen((track) { + if (currentMediaItem != null) { + if (track.duration != currentMediaItem!.duration && + (index! < queue.nvalue!.length && track.duration != null)) { + currentQueue![index!] = + currentQueue![index!].copyWith(duration: track.duration); + queue.add(currentQueue!); + } + mediaItem.add(currentMediaItem!); + } + }); + } + + @override + Future updateQueue(List queue) async { + this.queue.add(queue); + if (mediaItem.nvalue == null && + index != null && + index! >= 0 && + index! < queue.length) { + mediaItem.add(queue[index!]); + } + } + + Future customLoad(LoadRequest request) async { + _source = request.audioSourceMessage; + _updateShuffleIndices(); + _updateQueue(); + final response = await (await _player).load(LoadRequest( + audioSourceMessage: _source!, + initialPosition: request.initialPosition, + initialIndex: request.initialIndex, + )); + return LoadResponse(duration: response.duration); + } + + Future customSetVolume(SetVolumeRequest request) async => + await (await _player).setVolume(request); + + Future customSetSpeed(SetSpeedRequest request) async => + await (await _player).setSpeed(request); + + Future customSetPitch(SetPitchRequest request) async => + await (await _player).setPitch(request); + + Future customSetSkipSilence( + SetSkipSilenceRequest request) async => + await (await _player).setSkipSilence(request); + + Future customPlayerSeek(SeekRequest request) async => + await (await _player).seek(request); + + Future customSetShuffleOrder( + SetShuffleOrderRequest request) async { + _source = request.audioSourceMessage; + _updateShuffleIndices(); + _broadcastStateIfActive(); + return await (await _player).setShuffleOrder(SetShuffleOrderRequest( + audioSourceMessage: _source!, + )); + } + + Future customSetWebCrossOrigin( + SetWebCrossOriginRequest request) async { + return await (await _player).setWebCrossOrigin(request); + } + + Future customConcatenatingInsertAll( + ConcatenatingInsertAllRequest request) async { + final cat = _source!.findCat(request.id)!; + cat.children.insertAll(request.index, request.children); + _updateShuffleIndices(); + _broadcastStateIfActive(); + _updateQueue(); + return await (await _player).concatenatingInsertAll(request); + } + + Future customConcatenatingRemoveRange( + ConcatenatingRemoveRangeRequest request) async { + final cat = _source!.findCat(request.id)!; + cat.children.removeRange(request.startIndex, request.endIndex); + _updateShuffleIndices(); + _broadcastStateIfActive(); + _updateQueue(); + return await (await _player).concatenatingRemoveRange(request); + } + + Future customConcatenatingMove( + ConcatenatingMoveRequest request) async { + final cat = _source!.findCat(request.id)!; + cat.children + .insert(request.newIndex, cat.children.removeAt(request.currentIndex)); + _updateShuffleIndices(); + _broadcastStateIfActive(); + _updateQueue(); + return await (await _player).concatenatingMove(request); + } + + Future customSetAndroidAudioAttributes( + SetAndroidAudioAttributesRequest request) async => + await (await _player).setAndroidAudioAttributes(request); + + Future + customSetAutomaticallyWaitsToMinimizeStalling( + SetAutomaticallyWaitsToMinimizeStallingRequest request) async => + await (await _player) + .setAutomaticallyWaitsToMinimizeStalling(request); + + Future customAndroidEqualizerBandSetGain( + AndroidEqualizerBandSetGainRequest request) async => + await (await _player).androidEqualizerBandSetGain(request); + + Future + customAndroidEqualizerGetParameters( + AndroidEqualizerGetParametersRequest request) async => + await (await _player).androidEqualizerGetParameters(request); + + Future + customAndroidLoudnessEnhancerSetTargetGain( + AndroidLoudnessEnhancerSetTargetGainRequest request) async => + await (await _player).androidLoudnessEnhancerSetTargetGain(request); + + Future customAudioEffectSetEnabled( + AudioEffectSetEnabledRequest request) async => + await (await _player).audioEffectSetEnabled(request); + + Future customSetAllowsExternalPlayback( + SetAllowsExternalPlaybackRequest request) async => + await (await _player).setAllowsExternalPlayback(request); + + Future + customSetCanUseNetworkResourcesForLiveStreamingWhilePaused( + SetCanUseNetworkResourcesForLiveStreamingWhilePausedRequest + request) async => + await (await _player) + .setCanUseNetworkResourcesForLiveStreamingWhilePaused(request); + + Future customSetPreferredPeakBitRate( + SetPreferredPeakBitRateRequest request) async => + await (await _player).setPreferredPeakBitRate(request); + + void _updateQueue() { + assert(sequence.every((source) => source.tag is MediaItem), + 'Error : When using just_audio_background, you should always use a MediaItem as tag when setting an AudioSource. See AudioSource.uri documentation for more information.'); + queue.add(sequence.map((source) => source.tag as MediaItem).toList()); + } + + void _updateShuffleIndices() { + _shuffleIndices = _source?.shuffleIndices ?? []; + _effectiveIndices = _shuffleMode != AudioServiceShuffleMode.none + ? _shuffleIndices + : List.generate(sequence.length, (i) => i); + _shuffleIndicesInv = List.filled(_effectiveIndices.length, 0); + for (var i = 0; i < _effectiveIndices.length; i++) { + _shuffleIndicesInv[_effectiveIndices[i]] = i; + } + _effectiveIndicesInv = _shuffleMode != AudioServiceShuffleMode.none + ? _shuffleIndicesInv + : List.generate(sequence.length, (i) => i); + } + + List get sequence => _source?.sequence ?? []; + List get shuffleIndices => _shuffleIndices; + List get effectiveIndices => _effectiveIndices; + List get shuffleIndicesInv => _shuffleIndicesInv; + List get effectiveIndicesInv => _effectiveIndicesInv; + int get nextIndex => getRelativeIndex(1); + int get previousIndex => getRelativeIndex(-1); + bool get hasNext => nextIndex != -1; + bool get hasPrevious => previousIndex != -1; + + int getRelativeIndex(int offset) { + // if (_repeatMode == AudioServiceRepeatMode.one) return index!; + if (effectiveIndices.isEmpty) return -1; + if (index! >= effectiveIndicesInv.length) return -1; + final invPos = effectiveIndicesInv[index!]; + var newInvPos = invPos + offset; + if (newInvPos >= effectiveIndices.length || newInvPos < 0) { + if (_repeatMode == AudioServiceRepeatMode.all || + _repeatMode == AudioServiceRepeatMode.one) { + newInvPos %= effectiveIndices.length; + } else { + return -1; + } + } + final result = effectiveIndices[newInvPos]; + return result; + } + + @override + Future skipToQueueItem(int index) async { + (await _player).seek(SeekRequest(position: Duration.zero, index: index)); + } + + @override + Future skipToNext() async { + if (hasNext) { + await skipToQueueItem(nextIndex); + if (!_playing) { + await play(); + } + } + } + + @override + Future skipToPrevious() async { + if (hasPrevious) { + await skipToQueueItem(previousIndex); + if (!_playing) { + await play(); + } + } + } + + @override + Future play() async { + _updatePosition(); + _playing = true; + _broadcastState(); + await (await _player).play(PlayRequest()); + } + + @override + Future pause() async { + _updatePosition(); + _playing = false; + _broadcastState(); + await (await _player).pause(PauseRequest()); + } + + void _updatePosition() { + _justAudioEvent = _justAudioEvent.copyWith( + updatePosition: currentPosition, + updateTime: DateTime.now(), + ); + } + + @override + Future seek(Duration position) async => + await (await _player).seek(SeekRequest(position: position)); + + @override + Future setSpeed(double speed) async { + _speed = speed; + await (await _player).setSpeed(SetSpeedRequest(speed: speed)); + } + + @override + Future fastForward() => + _seekRelative(AudioService.config.fastForwardInterval); + + @override + Future rewind() => _seekRelative(-AudioService.config.rewindInterval); + + @override + Future seekForward(bool begin) async => _seekContinuously(begin, 1); + + @override + Future seekBackward(bool begin) async => _seekContinuously(begin, -1); + + @override + Future setRepeatMode(AudioServiceRepeatMode repeatMode) async { + _repeatMode = repeatMode; + _broadcastStateIfActive(); + (await _player).setLoopMode(SetLoopModeRequest( + loopMode: LoopModeMessage + .values[min(LoopModeMessage.values.length - 1, repeatMode.index)])); + } + + @override + Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { + _shuffleMode = shuffleMode; + _updateShuffleIndices(); + _broadcastStateIfActive(); + (await _player).setShuffleMode(SetShuffleModeRequest( + shuffleMode: ShuffleModeMessage.values[ + min(ShuffleModeMessage.values.length - 1, shuffleMode.index)])); + } + + @override + Future stop() async { + if (_justAudioEvent.processingState == ProcessingStateMessage.idle) { + return; + } + _updatePosition(); + _playing = false; + _broadcastState(); + // TODO: We should really stop listening to events here to mimic + // just_audio's behaviour. E.g. if stop() was called, we actually want to + // keep the state around even though the platform may be disposing its own + // state. + _platform.disposePlayer(DisposePlayerRequest(id: (await _player).id)); + _justAudioEvent = _justAudioEvent.copyWith( + processingState: ProcessingStateMessage.idle, + ); + } + + Duration get currentPosition { + if (_playing && + _justAudioEvent.processingState == ProcessingStateMessage.ready) { + return Duration( + milliseconds: (_justAudioEvent.updatePosition.inMilliseconds + + ((DateTime.now().millisecondsSinceEpoch - + _justAudioEvent.updateTime.millisecondsSinceEpoch) * + _speed)) + .toInt()); + } else { + return _justAudioEvent.updatePosition; + } + } + + /// Jumps away from the current position by [offset]. + Future _seekRelative(Duration offset) async { + var newPosition = currentPosition + offset; + // Make sure we don't jump out of bounds. + if (newPosition < Duration.zero) newPosition = Duration.zero; + if (newPosition > currentMediaItem!.duration!) { + newPosition = currentMediaItem!.duration!; + } + // Perform the jump via a seek. + await (await _player).seek(SeekRequest(position: newPosition)); + } + + /// Begins or stops a continuous seek in [direction]. After it begins it will + /// continue seeking forward or backward by 10 seconds within the audio, at + /// intervals of 1 second in app time. + void _seekContinuously(bool begin, int direction) { + _seeker?.stop(); + if (begin) { + _seeker = _SeamlessSeeker(this, Duration(seconds: 10 * direction), + const Duration(seconds: 1), currentMediaItem!.duration!) + ..start(); + } + } + + void _broadcastStateIfActive() { + if (_justAudioEvent.processingState != ProcessingStateMessage.idle) { + _broadcastState(); + } + } + + /// Broadcasts the current state to all clients. + void _broadcastState() { + final controls = [ + if (hasPrevious) MediaControl.skipToPrevious, + if (_playing) MediaControl.pause else MediaControl.play, + // MediaControl.stop, + if (hasNext) MediaControl.skipToNext, + ]; + playbackState.add(playbackState.nvalue!.copyWith( + controls: controls, + systemActions: { + MediaAction.seek, + MediaAction.seekForward, + MediaAction.seekBackward, + }, + androidCompactActionIndices: List.generate(controls.length, (i) => i) + .where((i) => controls[i].action != MediaAction.stop) + .toList(), + processingState: const { + ProcessingStateMessage.idle: AudioProcessingState.idle, + ProcessingStateMessage.loading: AudioProcessingState.loading, + ProcessingStateMessage.buffering: AudioProcessingState.buffering, + ProcessingStateMessage.ready: AudioProcessingState.ready, + ProcessingStateMessage.completed: AudioProcessingState.completed, + }[_justAudioEvent.processingState]!, + playing: _playing, + updatePosition: currentPosition, + bufferedPosition: _justAudioEvent.bufferedPosition, + speed: _speed, + queueIndex: _justAudioEvent.currentIndex, + )); + } +} + +class _SeamlessSeeker { + final _SeamlessPlayerAudioHandler handler; + final Duration positionInterval; + final Duration stepInterval; + final Duration duration; + bool _running = false; + + _SeamlessSeeker( + this.handler, + this.positionInterval, + this.stepInterval, + this.duration, + ); + + Future start() async { + _running = true; + while (_running) { + Duration newPosition = handler.currentPosition + positionInterval; + if (newPosition < Duration.zero) newPosition = Duration.zero; + if (newPosition > duration) newPosition = duration; + handler.seek(newPosition); + await Future.delayed(stepInterval); + } + } + + void stop() { + _running = false; + } +} + +extension _SeamlessPlaybackEventMessageExtension on PlaybackEventMessage { + PlaybackEventMessage copyWith({ + ProcessingStateMessage? processingState, + DateTime? updateTime, + Duration? updatePosition, + Duration? bufferedPosition, + Duration? duration, + IcyMetadataMessage? icyMetadata, + int? currentIndex, + int? androidAudioSessionId, + }) => + PlaybackEventMessage( + processingState: processingState ?? this.processingState, + updateTime: updateTime ?? this.updateTime, + updatePosition: updatePosition ?? this.updatePosition, + bufferedPosition: bufferedPosition ?? this.bufferedPosition, + duration: duration ?? this.duration, + icyMetadata: icyMetadata ?? this.icyMetadata, + currentIndex: currentIndex ?? this.currentIndex, + androidAudioSessionId: + androidAudioSessionId ?? this.androidAudioSessionId, + ); +} + +extension SeamlessAudioSourceExtension on AudioSourceMessage { + ConcatenatingAudioSourceMessage? findCat(String id) { + final self = this; + if (self is ConcatenatingAudioSourceMessage) { + if (self.id == id) return self; + return self.children + .map((child) => child.findCat(id)) + .firstWhere((cat) => cat != null, orElse: () => null); + } else if (self is LoopingAudioSourceMessage) { + return self.child.findCat(id); + } else { + return null; + } + } + + List get sequence { + final self = this; + if (self is ConcatenatingAudioSourceMessage) { + return self.children.expand((child) => child.sequence).toList(); + } else if (self is LoopingAudioSourceMessage) { + return List.generate(self.count, (i) => self.child.sequence) + .expand((sequence) => sequence) + .toList(); + } else { + return [self as IndexedAudioSourceMessage]; + } + } + + List get shuffleIndices { + final self = this; + if (self is ConcatenatingAudioSourceMessage) { + var offset = 0; + final childIndicesList = >[]; + for (final child in self.children) { + final childIndices = + child.shuffleIndices.map((i) => i + offset).toList(); + childIndicesList.add(childIndices); + offset += childIndices.length; + } + final indices = []; + for (final index in self.shuffleOrder) { + indices.addAll(childIndicesList[index]); + } + return indices; + } else if (self is LoopingAudioSourceMessage) { + // TODO: This should combine indices of the children, like ConcatenatingAudioSource. + // Also should be fixed in the plugin frontend. + return List.generate(self.count, (i) => i); + } else { + return [0]; + } + } +} + +@immutable +class SeamlessTrackInfo { + final int? index; + final Duration? duration; + + const SeamlessTrackInfo(this.index, this.duration); + + @override + bool operator ==(Object other) => + other is SeamlessTrackInfo && + index == other.index && + duration == other.duration; + + @override + int get hashCode => Object.hash(index, duration); + + @override + String toString() => '($index, $duration)'; +} + +/// Backwards compatible extensions on rxdart's ValueStream +extension _SeamlessValueStreamExtension on ValueStream { + /// Backwards compatible version of valueOrNull. + T? get nvalue => hasValue ? value : null; +} diff --git a/lib/src/service/search_api.dart b/lib/src/service/search_api.dart new file mode 100644 index 0000000..ff21b42 --- /dev/null +++ b/lib/src/service/search_api.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import '../models.dart'; +import '../utils.dart'; +import 'remote_api.dart'; + +class SearchApi { + const SearchApi({ + required this.keyword, + this.order = 'totalrank', + this.typeId = 0, + }) : assert(keyword != ''); + + final String keyword; + final String order; + final int typeId; + + Future> getPage( + {int pageKey = 1, int pageSize = 15}) async { + final response = await RemoteApi.getBaseSrc( + RemoteApiPath.search, + query: { + 'search_type': 'video', + 'keyword': keyword, + 'order': order, + 'tids': typeId.toString(), + 'page': pageKey.toString(), + 'page_size': pageSize.toString(), + }, + ); + + return await Future.wait( + (json.decode(response.body)['data']['result'] as List).map( + (jsonData) async { + if (jsonData['type'] != 'video') { + return AudioSummary.invalid(); + } + + final parts = (jsonData['duration'] as String).split(':'); + var minutes = int.parse(parts.first); + var seconds = int.parse(parts.last); + if (seconds > 0) { + seconds--; + } else if (minutes > 0) { + minutes--; + seconds = 59; + } + jsonData['duration'] = '$minutes:$seconds'; + + jsonData['pic'] = (await RemoteApi.get( + Format.validUri(jsonData['pic']), + )) + .bodyBytes; + return AudioSummary.fromJson(jsonData); + }, + ), + ); + } +} diff --git a/lib/src/service/setting_api.dart b/lib/src/service/setting_api.dart new file mode 100644 index 0000000..4565c6b --- /dev/null +++ b/lib/src/service/setting_api.dart @@ -0,0 +1,102 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingApi { + static final SettingApi _instance = SettingApi._(); + + factory SettingApi() => _instance; + + SettingApi._(); + + static Future init() async { + _instance._prefsWithCache = await SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions(), + ); + return; + } + + late final SharedPreferencesWithCache _prefsWithCache; + + static Future setBool( + String key, + bool value, + ) async => + await _instance._prefsWithCache.setBool(key, value); + static Future setDouble( + String key, + double value, + ) async => + await _instance._prefsWithCache.setDouble(key, value); + static Future setInt( + String key, + int value, + ) async => + await _instance._prefsWithCache.setInt(key, value); + static Future setString( + String key, + String value, + ) async => + await _instance._prefsWithCache.setString(key, value); + static Future setStringList( + String key, + List value, + ) async => + await _instance._prefsWithCache.setStringList(key, value); + + static bool? getBool(String key) => _instance._prefsWithCache.getBool(key); + static double? getDouble(String key) => + _instance._prefsWithCache.getDouble(key); + static int? getInt(String key) => _instance._prefsWithCache.getInt(key); + static String? getString(String key) => + _instance._prefsWithCache.getString(key); + static List? getStringList(String key) => + _instance._prefsWithCache.getStringList(key); + + static bool containsKey(String key) => + _instance._prefsWithCache.containsKey(key); + static Future remove(String key) async => + await _instance._prefsWithCache.remove(key); + + static Future addUp({ + required int uid, + String? tag, + String? pattern, + }) async { + await setStringList( + 'uids', + (getStringList('uids') ?? [])..add(uid.toString()), + ); + if (tag != null) { + await setString('up_tag_$uid', tag); + } + if (pattern != null) { + await setString('up_pattern_$uid', pattern); + } + } + + static Future removeUp(int uid) async { + await setStringList( + 'uids', + (getStringList('uids') ?? [])..remove(uid.toString()), + ); + if (containsKey('up_tag_$uid')) { + await remove('up_tag_$uid'); + } + if (containsKey('up_pattern_$uid')) { + await remove('up_pattern_$uid'); + } + } + + static List< + ({ + int uid, + String? tag, + String? pattern, + })> get Ups => [ + for (var uid in getStringList('uids') ?? []) + ( + uid: int.parse(uid), + tag: getString('up_tag_$uid'), + pattern: getString('up_pattern_$uid'), + ), + ]; +} diff --git a/lib/src/service/user_api.dart b/lib/src/service/user_api.dart new file mode 100644 index 0000000..518dab4 --- /dev/null +++ b/lib/src/service/user_api.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import '../models.dart'; +import '../utils.dart'; +import 'remote_api.dart'; + +class UserApi { + UserApi({ + required this.uid, + this.tag, + this.keywords = '', + }); + + final int uid; + final String? tag; + final String keywords; + final List verifiers = [ + (jsonData) => (jsonData['ugc_pay'] as int) == 0, + (jsonData) => !(jsonData['title'] as String).startsWith('【直播回放】'), + ]; + + Future get userSummary async { + final response = await RemoteApi.getBaseSrc( + RemoteApiPath.userInfo, + query: {'uids': uid.toString()}, + ); + + final jsonData = (jsonDecode(response.body)['data'] as List)[0]; + jsonData['face'] = (await RemoteApi.get( + Format.validUri(jsonData['face']), + )) + .bodyBytes; + jsonData['tag'] = tag; + + return UserSummary.fromJson(jsonData); + } + + void addVerifier(bool Function(dynamic) verifier) => verifiers.add(verifier); + + Future<({List audioSummaries, bool isLastPage})> getPage( + {int pageKey = 1, int pageSize = 15}) async { + final response = await RemoteApi.getBaseSrc( + RemoteApiPath.userSpace, + query: { + 'mid': uid.toString(), + 'keywords': keywords, + 'ps': pageSize.toString(), + 'pn': pageKey.toString(), + }, + ); + final jsonResponseData = json.decode(response.body)['data']; + final isLastPage = (jsonResponseData['page']['num'] as int) * + (jsonResponseData['page']['size'] as int) >= + (jsonResponseData['page']['total'] as int); + + return ( + audioSummaries: await Future.wait( + (jsonResponseData['archives'] as List).map( + (jsonData) async { + if (verifiers.any( + (verifier) => !verifier(jsonData), + )) { + return AudioSummary.invalid(); + } + + final viewResponse = await RemoteApi.getBaseSrc( + RemoteApiPath.audioInfo, + query: {'bvid': jsonData['bvid']}, + ); + var viewJson = json.decode(viewResponse.body)['data']; + if (viewJson['is_upower_exclusive'] as bool) { + return AudioSummary.invalid(); + } + + viewJson['author'] = viewJson['owner']['name'] as String; + viewJson['mid'] = uid; + viewJson['play'] = viewJson['stat']['view'] as int; + + var duration = (viewJson['duration'] as int); + if (duration > 0) { + duration--; + } + viewJson['duration'] = '${duration ~/ 60}:${(duration % 60)}'; + + viewJson['pic'] = (await RemoteApi.get( + Format.validUri(viewJson['pic']), + )) + .bodyBytes; + + viewJson['description'] = viewJson['desc'] as String; + viewJson['like'] = viewJson['stat']['like'] as int; + viewJson['favorites'] = viewJson['stat']['favorite'] as int; + + return AudioSummary.fromJson(viewJson); + }, + ), + ), + isLastPage: isLastPage, + ); + } +} diff --git a/lib/src/services.dart b/lib/src/services.dart new file mode 100644 index 0000000..85c6086 --- /dev/null +++ b/lib/src/services.dart @@ -0,0 +1,6 @@ +export 'service/remote_api.dart'; +export 'service/search_api.dart'; +export 'service/user_api.dart'; +export 'service/audio_play_api.dart'; +export 'service/setting_api.dart'; +export 'service/seamless_audio_handler.dart'; diff --git a/lib/src/util/display.dart b/lib/src/util/display.dart new file mode 100644 index 0000000..9e3a7ef --- /dev/null +++ b/lib/src/util/display.dart @@ -0,0 +1,62 @@ +import 'package:html_unescape/html_unescape_small.dart'; + +abstract class Display { + static String formatTitle(String title, {bool simplify = false}) { + final unescapedTitle = HtmlUnescape().convert(title); + return simplify + ? unescapedTitle.replaceAll(RegExp(r'^【.*?】'), '') + : unescapedTitle; + } + + static String formatPlay(int play) { + if (play < 10000) { + return play.toString(); + } + if (play < 100000000) { + return '${(play / 10000).toStringAsFixed(1)}万'; + } + return '${(play / 100000000).toStringAsFixed(1)}亿'; + } + + static String formatPubDate(DateTime pubDate) { + final now = DateTime.now(); + final difference = now.difference(pubDate); + + if (difference.inSeconds < 60) { + return '刚刚'; + } + if (difference.inMinutes < 60) { + return '${difference.inMinutes}分钟前'; + } + if (difference.inHours < 24) { + return '${difference.inHours}小时前'; + } + if (difference.inDays == 1) { + return '昨天 ${pubDate.hour}:${pubDate.minute.toString().padLeft(2, '0')}'; + } + if (difference.inDays < 4) { + return '${difference.inDays}天前'; + } + if (now.year == pubDate.year) { + return '${pubDate.month}月${pubDate.day}日'; + } + return '${pubDate.year}年${pubDate.month}月${pubDate.day}日'; + } + + static String formatDuration(String duration) { + if (duration == '') { + return duration; + } + + final parts = duration.split(':'); + if (parts.isEmpty) { + return duration; + } + if (int.parse(parts.first) >= 60) { + final hours = int.parse(parts.first) ~/ 60; + final minutes = int.parse(parts.first) % 60; + return '$hours:${minutes.toString().padLeft(2, '0')}:${parts.last.padLeft(2, '0')}'; + } + return '${parts.first}:${parts.last.padLeft(2, '0')}'; + } +} diff --git a/lib/src/util/format.dart b/lib/src/util/format.dart new file mode 100644 index 0000000..a318c56 --- /dev/null +++ b/lib/src/util/format.dart @@ -0,0 +1,10 @@ +abstract class Format { + static String validTitle(String title) => + title.replaceAll(RegExp(r'<[^>]*>'), ''); + + static Uri validUri(String uri) => Uri.parse( + uri.startsWith('//') + ? "https:$uri" + : (uri.replaceAll('http://', 'https://')), + ); +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 0000000..9586488 --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,2 @@ +export 'util/display.dart'; +export 'util/format.dart'; 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..4630625 --- /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 "railgun_minus") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.ezer.railgun_minus") + +# 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..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} 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..be1ee3e --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +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..b94d890 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,124 @@ +#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); + 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, "railgun_minus"); + 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, "railgun_minus"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(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 TRUE; +} + +// 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_NON_UNIQUE, + 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..c7529dd --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audio_service +import audio_session +import just_audio +import path_provider_foundation +import shared_preferences_foundation +import sqflite + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..602405f --- /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 /* railgun_minus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "railgun_minus.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 /* railgun_minus.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 /* railgun_minus.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.example.railgunMinus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/railgun_minus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/railgun_minus"; + }; + 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.example.railgunMinus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/railgun_minus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/railgun_minus"; + }; + 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.example.railgunMinus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/railgun_minus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/railgun_minus"; + }; + 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..d16919d --- /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..d53ef64 --- /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 true + } +} 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..49a7336 --- /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 = railgun_minus + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.railgunMinus + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 com.example. 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..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + 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 + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +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() + } +} 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..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +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..fadecb9 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,722 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + audio_service: + dependency: "direct main" + description: + name: audio_service + sha256: "9dd5ba7e77567b290c35908b1950d61485b4dfdd3a0ac398e98cfeec04651b75" + url: "https://pub.dev" + source: hosted + version: "0.18.15" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + sha256: "4cdc2127cd4562b957fb49227dc58e3303fafb09bde2573bc8241b938cf759d9" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" + url: "https://pub.dev" + source: hosted + version: "0.1.21" + 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" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + 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" + cronet_http: + dependency: "direct main" + description: + name: cronet_http + sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + 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: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77" + url: "https://pub.dev" + source: hosted + version: "0.14.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_native_splash: + dependency: "direct main" + description: + name: flutter_native_splash + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_staggered_grid_view: + dependency: transitive + 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" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + url: "https://pub.dev" + source: hosted + version: "14.2.7" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + html_unescape: + dependency: "direct main" + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + infinite_scroll_pagination: + dependency: "direct main" + description: + name: infinite_scroll_pagination + sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a + url: "https://pub.dev" + source: hosted + version: "4.0.0" + jni: + dependency: transitive + description: + name: jni + sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b + url: "https://pub.dev" + source: hosted + version: "0.10.1" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d + url: "https://pub.dev" + source: hosted + version: "0.9.40" + just_audio_platform_interface: + dependency: "direct main" + description: + name: just_audio_platform_interface + sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" + url: "https://pub.dev" + source: hosted + version: "4.3.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 + url: "https://pub.dev" + source: hosted + version: "0.4.12" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.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: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + 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: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + 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: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" + url: "https://pub.dev" + source: hosted + version: "2.5.4+3" + 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" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" + url: "https://pub.dev" + source: hosted + version: "3.3.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: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" + 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: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..d88ed10 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,118 @@ +name: railgun_minus +description: "A Bilibili Audio Player." +# 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.4.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 + + http: ^1.2.2 + cronet_http: ^1.3.2 + go_router: ^14.2.7 + shared_preferences: ^2.3.2 + path_provider: ^2.1.4 + path: ^1.9.0 + just_audio: ^0.9.40 + audio_service: ^0.18.15 + just_audio_platform_interface: ^4.3.0 + rxdart: ^0.28.0 + # audio_session: ^0.1.21 + # just_audio_background: ^0.0.1-beta.13 + infinite_scroll_pagination: ^4.0.0 + html_unescape: ^2.0.0 + crypto: ^3.0.5 + flutter_native_splash: ^2.4.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_launcher_icons: ^0.14.1 + + # 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 + +flutter_launcher_icons: + android: true + # ios: true + image_path: "assets/icon.png" + +flutter_native_splash: + color: "#ffffff" + color_dark: "#000000" + image: assets/icon.png + image_dark: assets/icon_dark.png + android: true + android_12: + image: assets/icon.png + image_dark: assets/icon_dark.png + +# 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..6dba28c --- /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:railgun_minus/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const App()); + + // 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/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..4b9d1b2 --- /dev/null +++ b/web/index.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + railgun_minus + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..ed8d341 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "railgun_minus", + "short_name": "railgun_minus", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/web/splash/img/dark-1x.png b/web/splash/img/dark-1x.png new file mode 100644 index 0000000..8a02ecb Binary files /dev/null and b/web/splash/img/dark-1x.png differ diff --git a/web/splash/img/dark-2x.png b/web/splash/img/dark-2x.png new file mode 100644 index 0000000..b681273 Binary files /dev/null and b/web/splash/img/dark-2x.png differ diff --git a/web/splash/img/dark-3x.png b/web/splash/img/dark-3x.png new file mode 100644 index 0000000..9316ed5 Binary files /dev/null and b/web/splash/img/dark-3x.png differ diff --git a/web/splash/img/dark-4x.png b/web/splash/img/dark-4x.png new file mode 100644 index 0000000..07c7e44 Binary files /dev/null and b/web/splash/img/dark-4x.png differ diff --git a/web/splash/img/light-1x.png b/web/splash/img/light-1x.png new file mode 100644 index 0000000..1745a95 Binary files /dev/null and b/web/splash/img/light-1x.png differ diff --git a/web/splash/img/light-2x.png b/web/splash/img/light-2x.png new file mode 100644 index 0000000..07efdcc Binary files /dev/null and b/web/splash/img/light-2x.png differ diff --git a/web/splash/img/light-3x.png b/web/splash/img/light-3x.png new file mode 100644 index 0000000..6e6d4cb Binary files /dev/null and b/web/splash/img/light-3x.png differ diff --git a/web/splash/img/light-4x.png b/web/splash/img/light-4x.png new file mode 100644 index 0000000..d34cd71 Binary files /dev/null and b/web/splash/img/light-4x.png differ 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..141c22e --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(railgun_minus 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 "railgun_minus") + +# 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..8b6d468 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} 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..3ad69c6 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +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..6e56dd6 --- /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.example" "\0" + VALUE "FileDescription", "railgun_minus" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "railgun_minus" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "railgun_minus.exe" "\0" + VALUE "ProductName", "railgun_minus" "\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..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +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()); + 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(); +} + +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. + 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..c7e5ffd --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // 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"railgun_minus", 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..3a0b465 --- /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(); + } + unsigned 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_