This commit is contained in:
曾志威
2026-05-16 01:10:06 +08:00
parent 65f0f52ef3
commit 9007ced788
153 changed files with 10709 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+19
View File
@@ -0,0 +1,19 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/async" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/collection" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/convert" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/core" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/developer" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/html" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/io" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/isolate" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/math" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/mirrors" />
<root url="file://C:\Users\zoe\AppData\Local\Programs\flutter/bin/cache/dart-sdk/lib/typed_data" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
+15
View File
@@ -0,0 +1,15 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime">
<CLASSES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib-sources.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect-sources.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test-sources.jar!/" />
</SOURCES>
</library>
</component>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/ttx_page_admin.iml" filepath="$PROJECT_DIR$/ttx_page_admin.iml" />
<module fileurl="file://$PROJECT_DIR$/android/ttx_page_admin_android.iml" filepath="$PROJECT_DIR$/android/ttx_page_admin_android.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method />
</configuration>
</component>
+45
View File
@@ -0,0 +1,45 @@
# 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: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: android
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: ios
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: linux
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: macos
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: web
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: windows
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
# 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'
+1
View File
@@ -0,0 +1 @@
C:/work/git/TtxPageAdmin/.symlink_test/target.txt
+1
View File
@@ -0,0 +1 @@
ok
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
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")
}
android {
namespace = "com.example.ttx_page_admin"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ttx_page_admin"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+45
View File
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="ttx_page_admin"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.ttx_page_admin
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
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 "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="jdk" jdkName="Android API 24 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Flutter for Android" level="project" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>
View File
View File
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+620
View File
@@ -0,0 +1,620 @@
// !$*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 */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.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.ttxPageAdmin;
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.ttxPageAdmin.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.ttxPageAdmin.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.ttxPageAdmin.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.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.ttxPageAdmin;
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.ttxPageAdmin;
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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -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.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Ttx Page Admin</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ttx_page_admin</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
File diff suppressed because it is too large Load Diff
+335
View File
@@ -0,0 +1,335 @@
part of 'studio_backend_client.dart';
class StudioBackendLoginResult {
const StudioBackendLoginResult({
required this.success,
required this.code,
required this.message,
required this.raw,
this.leftDays,
this.session,
this.appConfig,
this.systemConfig,
});
final bool success;
final String code;
final String message;
final int? leftDays;
final StudioBackendSession? session;
final Map<String, dynamic>? appConfig;
final Map<String, dynamic>? systemConfig;
final Map<String, dynamic> raw;
}
class StudioBackendLoginStatusResult {
const StudioBackendLoginStatusResult({
required this.success,
required this.code,
required this.message,
required this.raw,
this.leftDays,
this.session,
this.appConfig,
this.systemConfig,
});
final bool success;
final String code;
final String message;
final int? leftDays;
final StudioBackendSession? session;
final Map<String, dynamic>? appConfig;
final Map<String, dynamic>? systemConfig;
final Map<String, dynamic> raw;
}
class StudioBackendSession {
const StudioBackendSession({
required this.baseUrl,
required this.user,
required this.db,
required this.token,
required this.client,
required this.locale,
required this.timezone,
required this.portal,
required this.secretGen,
required this.sso,
required this.ssoToken,
required this.ssoApp,
required this.ssoClient,
required this.raw,
});
factory StudioBackendSession.initial({
required String baseUrl,
required String workspace,
required String locale,
}) {
return StudioBackendSession(
baseUrl: baseUrl,
user: 'default_user',
db: workspace,
token: 'default_token',
client: '',
locale: locale,
timezone: _localTimezoneCode(),
portal: '',
secretGen: '',
sso: '',
ssoToken: '',
ssoApp: '',
ssoClient: '',
raw: const <String, dynamic>{},
);
}
factory StudioBackendSession.fromJson({
required String baseUrl,
required Map<String, dynamic> raw,
required String fallbackUser,
required String fallbackWorkspace,
required String fallbackLocale,
}) {
return StudioBackendSession(
baseUrl: baseUrl,
user: _pick(raw, ['user', 'username'], fallbackUser),
db: _pick(raw, ['db', 'customer'], fallbackWorkspace),
token: _pick(raw, ['tk', 'token'], ''),
client: _pick(raw, ['client'], ''),
locale: _pick(raw, ['locale', 'language'], fallbackLocale),
timezone: _normalizeTimezone(_pick(raw, ['timezone'], '')),
portal: _pick(raw, ['portal'], ''),
secretGen: _pick(raw, ['secretGen'], ''),
sso: _pick(raw, ['sso'], ''),
ssoToken: _pick(raw, ['ssoToken'], ''),
ssoApp: _pick(raw, ['ssoApp'], ''),
ssoClient: _pick(raw, ['ssoClient'], ''),
raw: raw,
);
}
final String baseUrl;
final String user;
final String db;
final String token;
final String client;
final String locale;
final String timezone;
final String portal;
final String secretGen;
final String sso;
final String ssoToken;
final String ssoApp;
final String ssoClient;
final Map<String, dynamic> raw;
Map<String, String> toHeaders() {
final headers = <String, String>{};
void put(String key, String value) {
if (value.isNotEmpty) {
headers[key] = value;
}
}
put('X-user', user);
put('X-Token', token);
put('X-DB', db);
put('x-client', client);
put('x-locale', locale);
put('X-Portal', portal);
put('X-SSO', sso);
put('X-SSO-TOKEN', ssoToken);
put('X-SSO-APP', ssoApp);
put('X-SSO-CLIENT', ssoClient);
put('x-Timezone', _normalizeTimezone(timezone));
put('x-params', '{}');
if (secretGen.isNotEmpty) {
put('x-sec', secretGen);
}
return headers;
}
StudioBackendSession copyWith({
String? user,
String? db,
String? token,
String? client,
String? locale,
String? timezone,
String? portal,
String? secretGen,
String? sso,
String? ssoToken,
String? ssoApp,
String? ssoClient,
Map<String, dynamic>? raw,
}) {
return StudioBackendSession(
baseUrl: baseUrl,
user: user ?? this.user,
db: db ?? this.db,
token: token ?? this.token,
client: client ?? this.client,
locale: locale ?? this.locale,
timezone: timezone ?? this.timezone,
portal: portal ?? this.portal,
secretGen: secretGen ?? this.secretGen,
sso: sso ?? this.sso,
ssoToken: ssoToken ?? this.ssoToken,
ssoApp: ssoApp ?? this.ssoApp,
ssoClient: ssoClient ?? this.ssoClient,
raw: raw ?? this.raw,
);
}
}
class StudioBackendUserProfile {
const StudioBackendUserProfile({
required this.code,
required this.name,
required this.isDeveloper,
required this.portal,
required this.portraitUrl,
required this.raw,
});
factory StudioBackendUserProfile.fromJson(Map<String, dynamic> raw) {
return StudioBackendUserProfile(
code: _pick(raw, ['code', 'userCode', 'id'], ''),
name: _pick(raw, ['name', 'userName'], ''),
isDeveloper: _pick(raw, ['isDeveloper'], 'false').toLowerCase() == 'true',
portal: _pick(raw, ['portal'], ''),
portraitUrl: _pick(raw, ['portraitUrl'], ''),
raw: raw,
);
}
final String code;
final String name;
final bool isDeveloper;
final String portal;
final String portraitUrl;
final Map<String, dynamic> raw;
}
class StudioBackendOrg {
const StudioBackendOrg({
required this.code,
required this.name,
required this.raw,
});
factory StudioBackendOrg.fromJson(Map<String, dynamic> raw) {
return StudioBackendOrg(
code: _pick(raw, ['code', 'orgCode'], ''),
name: _pick(raw, ['name', 'orgName'], ''),
raw: raw,
);
}
final String code;
final String name;
final Map<String, dynamic> raw;
}
class StudioBackendActionResult {
const StudioBackendActionResult({
required this.success,
required this.code,
required this.message,
required this.data,
required this.raw,
});
final bool success;
final String code;
final String message;
final dynamic data;
final Map<String, dynamic> raw;
}
class StudioBackendDownloadResult {
const StudioBackendDownloadResult({
required this.bytes,
required this.filename,
required this.contentType,
required this.statusCode,
required this.rawHeaders,
});
final Uint8List bytes;
final String filename;
final String contentType;
final int statusCode;
final Map<String, List<String>> rawHeaders;
}
class _BackendJsonResponse {
const _BackendJsonResponse({required this.json, required this.statusCode});
factory _BackendJsonResponse.fromResponse(Response<dynamic> response) {
final data = response.data;
final statusCode = response.statusCode ?? 0;
final location = response.headers.value('location') ?? '';
if (data is String) {
try {
final decoded = jsonDecode(data);
if (decoded is Map<String, dynamic>) {
return _BackendJsonResponse(
json: _withHttpMeta(decoded, statusCode, location),
statusCode: statusCode,
);
}
return _BackendJsonResponse(
json: _withHttpMeta(
<String, dynamic>{'data': decoded},
statusCode,
location,
),
statusCode: statusCode,
);
} catch (_) {
return _BackendJsonResponse(
json: _withHttpMeta(
<String, dynamic>{'raw': data},
statusCode,
location,
),
statusCode: statusCode,
);
}
}
if (data is Map<String, dynamic>) {
return _BackendJsonResponse(
json: _withHttpMeta(data, statusCode, location),
statusCode: statusCode,
);
}
return _BackendJsonResponse(
json: _withHttpMeta(
<String, dynamic>{'data': data},
statusCode,
location,
),
statusCode: statusCode,
);
}
static Map<String, dynamic> _withHttpMeta(
Map<String, dynamic> json,
int statusCode,
String location,
) {
return <String, dynamic>{
...json,
'_httpStatus': statusCode,
if (location.isNotEmpty) '_location': location,
};
}
final Map<String, dynamic> json;
final int statusCode;
}
+63
View File
@@ -0,0 +1,63 @@
part of 'studio_backend_client.dart';
String _localTimezoneCode() {
final now = DateTime.now();
final timezoneName = now.timeZoneName.trim();
if (timezoneName.isNotEmpty) {
final normalizedTimezone = _normalizeTimezone(timezoneName);
if (normalizedTimezone != timezoneName) {
return normalizedTimezone;
}
}
final offset = now.timeZoneOffset;
if (offset.inHours == 8 && offset.inMinutes.remainder(60) == 0) {
return 'Asia/Shanghai';
}
final totalMinutes = offset.inMinutes;
final sign = totalMinutes < 0 ? '-' : '+';
final absoluteMinutes = totalMinutes.abs();
final hours = (absoluteMinutes ~/ 60).toString().padLeft(2, '0');
final minutes = (absoluteMinutes % 60).toString().padLeft(2, '0');
return '$sign$hours:$minutes';
}
String _normalizeTimezone(String value) {
final timezone = value.trim();
if (timezone.isEmpty) {
return _localTimezoneCode();
}
const windowsTimezoneMap = <String, String>{
'中国标准时间': 'Asia/Shanghai',
'China Standard Time': 'Asia/Shanghai',
'Taipei Standard Time': 'Asia/Taipei',
'Tokyo Standard Time': 'Asia/Tokyo',
'Korea Standard Time': 'Asia/Seoul',
'Singapore Standard Time': 'Asia/Singapore',
'UTC': 'UTC',
};
final mappedTimezone = windowsTimezoneMap[timezone];
if (mappedTimezone != null) {
return mappedTimezone;
}
final asciiTimezone = RegExp(r'^[\x20-\x7E]+$').hasMatch(timezone);
if (asciiTimezone) {
return timezone;
}
return 'Asia/Shanghai';
}
String _pick(Map<String, dynamic> map, List<String> keys, String fallback) {
for (final key in keys) {
final value = map[key];
if (value != null && value.toString().isNotEmpty) {
return value.toString();
}
}
return fallback;
}
+7
View File
@@ -0,0 +1,7 @@
export 'studio_app.dart';
import 'studio_app.dart' as studio;
void main() {
studio.runStudioApp();
}
+2
View File
@@ -0,0 +1,2 @@
export 'studio_download_saver_stub.dart'
if (dart.library.io) 'studio_download_saver_io.dart';
@@ -0,0 +1,24 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:file_selector/file_selector.dart';
// 桌面端保存下载文件:打开系统“另存为”对话框并落盘。
Future<String?> saveBytesToFile({
required Uint8List bytes,
required String suggestedName,
}) async {
final location = await getSaveLocation(
suggestedName: suggestedName,
acceptedTypeGroups: <XTypeGroup>[
XTypeGroup(label: 'Zip', extensions: <String>['zip']),
],
);
final path = location?.path;
if (path == null || path.trim().isEmpty) {
return null;
}
final file = File(path);
await file.writeAsBytes(bytes, flush: true);
return path;
}
@@ -0,0 +1,8 @@
import 'dart:typed_data';
Future<String?> saveBytesToFile({
required Uint8List bytes,
required String suggestedName,
}) async {
return null;
}
+24
View File
@@ -0,0 +1,24 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'backend/studio_backend_client.dart';
import 'platform/studio_download_saver.dart';
part 'studio_app_theme.dart';
part 'studio_app_models.dart';
part 'studio_app_entry.dart';
part 'studio_app_login.dart';
part 'studio_app_shell.dart';
part 'studio_app_shell_sidebar.dart';
part 'studio_app_shell_workspace.dart';
part 'studio_app_pages.dart';
part 'studio_app_pages_history.dart';
part 'studio_app_pages_operations.dart';
part 'studio_app_pages_sql.dart';
part 'studio_app_pages_form_editor.dart';
part 'studio_app_pages_portal.dart';
void runStudioApp() {
runApp(const StudioApp());
}
+40
View File
@@ -0,0 +1,40 @@
part of 'studio_app.dart';
class StudioApp extends StatefulWidget {
const StudioApp({super.key});
@override
State<StudioApp> createState() => _StudioAppState();
}
class _StudioAppState extends State<StudioApp> {
ThemeMode _themeMode = ThemeMode.light;
void _toggleTheme() {
setState(() {
_themeMode = _themeMode == ThemeMode.dark
? ThemeMode.light
: ThemeMode.dark;
});
}
@override
Widget build(BuildContext context) {
final languageCode = Uri.base.queryParameters['lang'] ?? 'zh';
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TTX Studio',
theme: buildStudioTheme(Brightness.light),
darkTheme: buildStudioTheme(Brightness.dark),
themeMode: _themeMode,
home: SelectionArea(
child: StudioBootstrap(
languageCode: languageCode,
themeMode: _themeMode,
onToggleTheme: _toggleTheme,
),
),
);
}
}
+551
View File
@@ -0,0 +1,551 @@
part of 'studio_app.dart';
class StudioBootstrap extends StatefulWidget {
const StudioBootstrap({
super.key,
required this.languageCode,
required this.themeMode,
required this.onToggleTheme,
});
final String languageCode;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
@override
State<StudioBootstrap> createState() => _StudioBootstrapState();
}
class _StudioBootstrapState extends State<StudioBootstrap> {
StudioRuntime? _runtime;
void _handleLogin(StudioRuntime runtime) {
setState(() {
_runtime = runtime;
});
}
void _handleLogout() {
final runtime = _runtime;
if (runtime == null) {
return;
}
runtime.client
.logout(session: runtime.session)
.catchError((_) {})
.whenComplete(() {
if (!mounted) {
return;
}
setState(() {
_runtime = null;
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 260),
child: _runtime == null
? StudioLoginView(
key: const ValueKey('studio-login'),
languageCode: widget.languageCode,
themeMode: widget.themeMode,
onToggleTheme: widget.onToggleTheme,
onLogin: _handleLogin,
)
: StudioShell(
key: const ValueKey('studio-shell'),
runtime: _runtime!,
languageCode: widget.languageCode,
themeMode: widget.themeMode,
onToggleTheme: widget.onToggleTheme,
onLogout: _handleLogout,
),
);
}
}
class StudioLoginView extends StatefulWidget {
const StudioLoginView({
super.key,
required this.languageCode,
required this.themeMode,
required this.onToggleTheme,
required this.onLogin,
});
final String languageCode;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final ValueChanged<StudioRuntime> onLogin;
@override
State<StudioLoginView> createState() => _StudioLoginViewState();
}
class _StudioLoginViewState extends State<StudioLoginView> {
late final TextEditingController _backendUrlController;
late final TextEditingController _workspaceController;
late final TextEditingController _usernameController;
late final TextEditingController _passwordController;
bool _loading = false;
String? _errorMessage;
Map<String, dynamic> _loginConfig = <String, dynamic>{};
@override
void initState() {
super.initState();
_backendUrlController = TextEditingController(
text: 'http://localhost:9001/',
);
_workspaceController = TextEditingController(text: _defaultWorkspace);
_usernameController = TextEditingController();
_passwordController = TextEditingController();
_loadLoginConfig();
}
@override
void dispose() {
_backendUrlController.dispose();
_workspaceController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _loadLoginConfig() async {
try {
final client = StudioBackendClient(
baseUrl: _backendUrlController.text,
languageCode: widget.languageCode,
);
final config = await client.fetchLoginPageConfig();
if (!mounted) {
return;
}
setState(() {
_loginConfig = config ?? <String, dynamic>{};
});
_applyLoginConfig(_loginConfig);
} catch (error, stackTrace) {
_printFullError('加载登录配置失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_errorMessage = '后端配置暂时不可用,仍可继续尝试登录';
});
}
}
Future<void> _submit() async {
final workspace = _normalizeWorkspace(_workspaceController.text);
final username = _usernameController.text.trim();
final password = _passwordController.text;
final backendUrl = _backendUrlController.text.trim();
if (workspace.isEmpty || username.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('请先填写工作区、用户名和密码')));
return;
}
if (_workspaceController.text.trim() != workspace) {
_workspaceController.text = workspace;
}
if (_loading) {
return;
}
setState(() {
_loading = true;
_errorMessage = null;
});
try {
final client = StudioBackendClient(
baseUrl: backendUrl,
languageCode: widget.languageCode,
);
final loginResult = await client.login(
workspace: workspace,
username: username,
password: password,
authType: '',
);
if (!loginResult.success || loginResult.session == null) {
final fallbackMessage = loginResult.success
? '登录成功但未返回会话信息'
: '后端登录失败,code=${loginResult.code.isEmpty ? '' : loginResult.code}';
throw _LoginFailureException(
loginResult.message.isNotEmpty
? loginResult.message
: fallbackMessage,
code: loginResult.code,
raw: loginResult.raw,
);
}
final session = loginResult.session!;
final profile = await client.fetchUserProfile(session);
final org = await client.fetchOrgByUser(session);
final menuIds = await client.fetchPermissionIds(session);
final appName =
_extractAppName(loginResult.appConfig) ??
_extractAppName(loginResult.systemConfig) ??
_extractAppName(_loginConfig);
final organizationName = org?.name ?? '';
final backendAppName = appName ?? 'TTX Studio';
if (!mounted) {
return;
}
widget.onLogin(
StudioRuntime(
backendUrl: backendUrl,
client: client,
session: session,
user: StudioUser(
workspace: session.db.isNotEmpty ? session.db : workspace,
displayName: (profile?.name.isNotEmpty ?? false)
? profile!.name
: username,
role: profile?.isDeveloper == true ? '开发者' : '管理员',
languageCode: widget.languageCode,
backendAppName: backendAppName,
organizationName: organizationName,
),
loginConfig: loginResult.appConfig ?? _loginConfig,
systemConfig: loginResult.systemConfig ?? const <String, dynamic>{},
orgName: organizationName,
allowedMenuIds: menuIds,
profile: profile,
),
);
} catch (error, stackTrace) {
_printFullError('登录失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_errorMessage = _normalizeErrorMessage(error);
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(_errorMessage ?? '登录失败')));
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
void _applyLoginConfig(Map<String, dynamic> config) {
final backendUrl = _extractStringFromConfig(config, const [
'server',
'url',
'loginPageServer',
]);
final workspace = _extractStringFromConfig(config, const [
'db',
'customer',
'workspace',
]);
if (backendUrl.isNotEmpty &&
_backendUrlController.text.trim() == 'http://localhost:9001/') {
_backendUrlController.text = backendUrl;
}
if (workspace.isNotEmpty &&
_workspaceController.text.trim() == _defaultWorkspace) {
_workspaceController.text = _normalizeWorkspace(workspace);
}
}
String _normalizeWorkspace(String value) {
final workspace = value.trim();
if (workspace == 'ttx-xwms-tes') {
return _defaultWorkspace;
}
return workspace;
}
String _normalizeErrorMessage(Object error) {
if (error is _LoginFailureException) {
return error.message;
}
final text = error.toString();
const marker = 'Bad state: ';
if (text.startsWith(marker)) {
return text.substring(marker.length);
}
return text;
}
void _printFullError(String title, Object error, StackTrace stackTrace) {
final buffer = StringBuffer()
..writeln('========== $title ==========')
..writeln('error: $error')
..writeln('stackTrace:')
..writeln(stackTrace);
if (error is _LoginFailureException) {
buffer
..writeln('code: ${error.code.isEmpty ? '' : error.code}')
..writeln('raw response:')
..writeln(const JsonEncoder.withIndent(' ').convert(error.raw));
}
buffer.writeln('========== $title end ===========');
_debugPrintLong(buffer.toString());
}
void _debugPrintLong(String text) {
const chunkSize = 900;
for (var start = 0; start < text.length; start += chunkSize) {
final end = (start + chunkSize).clamp(0, text.length);
debugPrint(text.substring(start, end));
}
}
@override
Widget build(BuildContext context) {
final backendHint = _backendUrlController.text.trim().isEmpty
? 'http://localhost:9001/'
: _backendUrlController.text.trim();
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _loginGradientColors(context),
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Stack(
children: [
Positioned(
right: 20,
top: 20,
child: SafeArea(
child: _ThemeToggleButton(
themeMode: widget.themeMode,
onPressed: widget.onToggleTheme,
),
),
),
Positioned(
left: -120,
top: -120,
child: _GlowCircle(
color: const Color(0xFF1AA6A6).withOpacity(0.20),
),
),
Positioned(
right: -100,
bottom: -120,
child: _GlowCircle(
color: const Color(0xFFFFA94D).withOpacity(0.16),
),
),
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(24),
child: _LoginForm(
backendUrlController: _backendUrlController,
workspaceController: _workspaceController,
usernameController: _usernameController,
passwordController: _passwordController,
loading: _loading,
backendHint: backendHint,
errorMessage: _errorMessage,
onSubmit: _submit,
),
),
),
),
),
),
),
],
),
),
);
}
}
class _LoginFailureException implements Exception {
const _LoginFailureException(
this.message, {
required this.code,
required this.raw,
});
final String message;
final String code;
final Map<String, dynamic> raw;
@override
String toString() => message;
}
class _LoginForm extends StatelessWidget {
const _LoginForm({
required this.backendUrlController,
required this.workspaceController,
required this.usernameController,
required this.passwordController,
required this.loading,
required this.backendHint,
required this.errorMessage,
required this.onSubmit,
});
final TextEditingController backendUrlController;
final TextEditingController workspaceController;
final TextEditingController usernameController;
final TextEditingController passwordController;
final bool loading;
final String backendHint;
final String? errorMessage;
final VoidCallback onSubmit;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: _isDarkTheme(context) ? const Color(0xFF0C1524) : Colors.white,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'后端登录',
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'连接真实后端后,登录、权限菜单、用户信息和退出都会走服务端。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
const SizedBox(height: 16),
TextField(
controller: backendUrlController,
decoration: InputDecoration(
labelText: '后端地址',
hintText: backendHint,
prefixIcon: const Icon(Icons.link_rounded),
),
keyboardType: TextInputType.url,
onChanged: (_) {},
),
if (errorMessage != null && errorMessage!.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.12),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.red.withOpacity(0.25)),
),
child: Text(
errorMessage!,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.redAccent),
),
),
],
const SizedBox(height: 20),
TextField(
controller: workspaceController,
decoration: const InputDecoration(
labelText: '工作区',
prefixIcon: Icon(Icons.domain_rounded),
),
),
const SizedBox(height: 14),
TextField(
controller: usernameController,
decoration: const InputDecoration(
labelText: '用户名',
prefixIcon: Icon(Icons.person_rounded),
),
),
const SizedBox(height: 14),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock_rounded),
),
onSubmitted: (_) => loading ? null : onSubmit(),
),
const SizedBox(height: 18),
FilledButton.icon(
onPressed: loading ? null : onSubmit,
icon: loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.login_rounded),
label: Text(loading ? '正在连接...' : '进入 Studio'),
),
const SizedBox(height: 14),
Text(
'默认连接到后端服务,登录后会进入桌面工作台。',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.58)),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: scheme.primary.withOpacity(0.12),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Icon(Icons.desktop_windows_rounded, color: scheme.primary),
const SizedBox(width: 12),
Expanded(
child: Text(
'打开后会进入桌面工作台,支持导航、标签页和编辑器。',
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
),
],
),
);
}
}
+361
View File
@@ -0,0 +1,361 @@
part of 'studio_app.dart';
const String _defaultWorkspace = 'ttx-xwms-test';
class StudioRuntime {
const StudioRuntime({
required this.backendUrl,
required this.client,
required this.session,
required this.user,
required this.loginConfig,
required this.systemConfig,
required this.orgName,
required this.allowedMenuIds,
required this.profile,
});
final String backendUrl;
final StudioBackendClient client;
final StudioBackendSession session;
final StudioUser user;
final Map<String, dynamic> loginConfig;
final Map<String, dynamic> systemConfig;
final String orgName;
final Set<String> allowedMenuIds;
final StudioBackendUserProfile? profile;
}
class StudioUser {
const StudioUser({
required this.workspace,
required this.displayName,
required this.role,
required this.languageCode,
required this.backendAppName,
required this.organizationName,
});
final String workspace;
final String displayName;
final String role;
final String languageCode;
final String backendAppName;
final String organizationName;
}
class StudioActivity {
StudioActivity({required this.title, required this.detail})
: time = DateTime.now();
final String title;
final String detail;
final DateTime time;
}
class StudioTab {
StudioTab({
required this.id,
required this.title,
required this.summary,
required this.pageType,
required this.icon,
});
static const String dashboardId = 'dashboard';
factory StudioTab.dashboard() {
return StudioTab(
id: dashboardId,
title: '工作台',
summary: 'Studio 首页和快捷入口',
pageType: StudioPageType.dashboard,
icon: Icons.space_dashboard_rounded,
);
}
final String id;
final String title;
final String summary;
final StudioPageType pageType;
final IconData icon;
}
enum StudioPageType {
dashboard,
modelEditor,
jsonEditor,
navigationEditor,
manage,
history,
sync,
repair,
sqlTemplate,
sqlRunner,
portalMaintain,
}
class StudioNavNode {
StudioNavNode({
required this.id,
required this.title,
required this.summary,
required this.icon,
required this.pageType,
this.children = const [],
});
final String id;
final String title;
final String summary;
final IconData icon;
final StudioPageType pageType;
final List<StudioNavNode> children;
}
class _HistoryRow {
const _HistoryRow(this.version, this.user, this.time, this.status, this.note);
final String version;
final String user;
final String time;
final String status;
final String note;
}
final List<StudioNavNode> studioNavTree = [
StudioNavNode(
id: 'model-center',
title: '模型中心',
summary: '数据、单据、界面和导航',
icon: Icons.account_tree_rounded,
pageType: StudioPageType.dashboard,
children: [
StudioNavNode(
id: 'TableModelEditor',
title: '数据模型',
summary: '字段、索引、关系与版本',
icon: Icons.table_rows_rounded,
pageType: StudioPageType.modelEditor,
),
StudioNavNode(
id: 'BillModelEditor',
title: '单据模型',
summary: '业务单据结构和规则',
icon: Icons.description_rounded,
pageType: StudioPageType.modelEditor,
),
StudioNavNode(
id: 'ViewModelEditor',
title: '界面模型',
summary: '界面布局和控件配置',
icon: Icons.dashboard_customize_rounded,
pageType: StudioPageType.modelEditor,
),
StudioNavNode(
id: 'NavigationEditor',
title: '菜单维护',
summary: '导航结构和入口配置',
icon: Icons.menu_rounded,
pageType: StudioPageType.navigationEditor,
),
],
),
StudioNavNode(
id: 'json-center',
title: '模型 JSON',
summary: '直写 JSON 与脚本化编辑',
icon: Icons.data_object_rounded,
pageType: StudioPageType.dashboard,
children: [
StudioNavNode(
id: 'TableJsonEditor',
title: '数据模型 Json',
summary: 'JSON 结构和字段定义',
icon: Icons.data_object_rounded,
pageType: StudioPageType.jsonEditor,
),
StudioNavNode(
id: 'BillJsonEditor',
title: '单据模型 Json',
summary: '单据配置的 JSON 编辑器',
icon: Icons.code_rounded,
pageType: StudioPageType.jsonEditor,
),
StudioNavNode(
id: 'ViewJsonEditor',
title: '界面模型 Json',
summary: '界面配置的 JSON 编辑器',
icon: Icons.article_rounded,
pageType: StudioPageType.jsonEditor,
),
StudioNavNode(
id: 'NavigatorEditor',
title: '菜单维护 Json',
summary: '导航树的 JSON 配置',
icon: Icons.list_alt_rounded,
pageType: StudioPageType.navigationEditor,
),
],
),
StudioNavNode(
id: 'history',
title: '模型日志',
summary: '版本与修改历史',
icon: Icons.history_rounded,
pageType: StudioPageType.dashboard,
children: [
StudioNavNode(
id: 'TableModelHistory',
title: '数据模型日志',
summary: '数据模型修改历史',
icon: Icons.query_builder_rounded,
pageType: StudioPageType.history,
),
StudioNavNode(
id: 'BillModelHistory',
title: '单据模型日志',
summary: '单据模型修改历史',
icon: Icons.history_toggle_off_rounded,
pageType: StudioPageType.history,
),
StudioNavNode(
id: 'WsoDataHistory',
title: '界面模型日志',
summary: '界面模型修改历史',
icon: Icons.event_note_rounded,
pageType: StudioPageType.history,
),
StudioNavNode(
id: 'NavigatorHistory',
title: '菜单维护日志',
summary: '菜单配置修改历史',
icon: Icons.receipt_long_rounded,
pageType: StudioPageType.history,
),
],
),
StudioNavNode(
id: 'manage',
title: '模型管理',
summary: '在线维护与发布',
icon: Icons.tune_rounded,
pageType: StudioPageType.dashboard,
children: [
StudioNavNode(
id: 'WsoDataManage',
title: '界面模型管理',
summary: '界面模型维护',
icon: Icons.preview_rounded,
pageType: StudioPageType.manage,
),
StudioNavNode(
id: 'BillModelManage',
title: '单据模型管理',
summary: '单据模型维护',
icon: Icons.inventory_2_rounded,
pageType: StudioPageType.manage,
),
StudioNavNode(
id: 'TableModelManage',
title: '数据模型管理',
summary: '数据模型维护',
icon: Icons.storage_rounded,
pageType: StudioPageType.manage,
),
],
),
StudioNavNode(
id: 'ModelSync',
title: '配置下载',
summary: '同步和导出配置',
icon: Icons.file_download_rounded,
pageType: StudioPageType.sync,
),
StudioNavNode(
id: 'DataRepair',
title: '数据修复',
summary: '异常数据处理和修复',
icon: Icons.build_rounded,
pageType: StudioPageType.repair,
),
StudioNavNode(
id: 'SqlTemplate',
title: 'SQL 模板',
summary: '查询模板和脚本管理',
icon: Icons.description_rounded,
pageType: StudioPageType.sqlTemplate,
),
StudioNavNode(
id: 'AdvancedSqlTemplate',
title: 'SQL 模板执行',
summary: '直接执行查询模板',
icon: Icons.play_arrow_rounded,
pageType: StudioPageType.sqlRunner,
),
StudioNavNode(
id: 'PortalMaintain',
title: 'Portal 配置',
summary: '门户和主题管理',
icon: Icons.settings_applications_rounded,
pageType: StudioPageType.portalMaintain,
),
];
List<StudioNavNode> _filterNavNodes(
List<StudioNavNode> nodes,
Set<String> allowedIds,
) {
if (allowedIds.isEmpty) {
return nodes;
}
final filtered = <StudioNavNode>[];
for (final node in nodes) {
final filteredChildren = node.children.isEmpty
? const <StudioNavNode>[]
: _filterNavNodes(node.children, allowedIds);
final allowed = allowedIds.contains(node.id);
if (allowed || filteredChildren.isNotEmpty) {
filtered.add(
StudioNavNode(
id: node.id,
title: node.title,
summary: node.summary,
icon: node.icon,
pageType: node.pageType,
children: filteredChildren,
),
);
}
}
return filtered;
}
String _extractStringFromConfig(
Map<String, dynamic> config,
List<String> keys,
) {
for (final key in keys) {
final value = config[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString().trim();
}
}
return '';
}
String? _extractAppName(Map<String, dynamic>? config) {
if (config == null || config.isEmpty) {
return null;
}
final candidate = _extractStringFromConfig(config, const [
'appName',
'name',
'title',
'portalName',
'systemName',
]);
return candidate.isEmpty ? null : candidate;
}
+388
View File
@@ -0,0 +1,388 @@
part of 'studio_app.dart';
class StudioDashboardPage extends StatelessWidget {
const StudioDashboardPage({
super.key,
required this.tab,
required this.user,
required this.activities,
required this.rootNodes,
required this.onOpenNode,
});
final StudioTab tab;
final StudioUser user;
final List<StudioActivity> activities;
final List<StudioNavNode> rootNodes;
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
final quickNodes = _allLeaves(rootNodes).take(6).toList();
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(
context,
).colorScheme.primary.withOpacity(0.25),
Theme.of(
context,
).colorScheme.tertiary.withOpacity(0.20),
],
),
borderRadius: BorderRadius.circular(24),
),
child: const Icon(
Icons.space_dashboard_rounded,
size: 38,
),
),
const SizedBox(width: 18),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'工作台',
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
'左侧菜单已映射自 studio.html 的核心入口,点击即可打开多标签页。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.white70),
),
],
),
),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
_MiniBadge(text: user.workspace),
_MiniBadge(text: user.languageCode.toUpperCase()),
],
),
],
),
),
),
const SizedBox(height: 14),
LayoutBuilder(
builder: (context, constraints) {
final cardWidth = constraints.maxWidth >= 1100
? (constraints.maxWidth - 32) / 3
: constraints.maxWidth >= 760
? (constraints.maxWidth - 16) / 2
: constraints.maxWidth;
return Wrap(
spacing: 16,
runSpacing: 16,
children: [
SizedBox(
width: cardWidth,
child: _InfoPanel(
title: '会话状态',
value: user.displayName,
icon: Icons.person_rounded,
subtitle: '${user.role} · 已登录',
),
),
SizedBox(
width: cardWidth,
child: _InfoPanel(
title: '打开页面',
value: '${activities.length} 条记录',
icon: Icons.tab_rounded,
subtitle: '支持多标签并行工作',
),
),
SizedBox(
width: cardWidth,
child: _InfoPanel(
title: '运行方式',
value: '桌面端',
icon: Icons.desktop_windows_rounded,
subtitle: 'Flutter Windows / Web 共用',
),
),
],
);
},
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'快捷入口',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: quickNodes
.map(
(node) => ActionChip(
avatar: Icon(node.icon, size: 18),
label: Text(node.title),
onPressed: () => onOpenNode(node),
),
)
.toList(),
),
],
),
),
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
final leftWidth = constraints.maxWidth >= 980
? (constraints.maxWidth - 16) * 0.58
: constraints.maxWidth;
final rightWidth = constraints.maxWidth >= 980
? (constraints.maxWidth - 16) * 0.42
: constraints.maxWidth;
if (constraints.maxWidth >= 980) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: leftWidth,
child: _ActivityPanel(activities: activities),
),
const SizedBox(width: 16),
SizedBox(
width: rightWidth,
child: _FeaturePanel(onOpenNode: onOpenNode),
),
],
);
}
return Column(
children: [
_ActivityPanel(activities: activities),
const SizedBox(height: 16),
_FeaturePanel(onOpenNode: onOpenNode),
],
);
},
),
],
),
),
);
}
List<StudioNavNode> _allLeaves(List<StudioNavNode> nodes) {
final result = <StudioNavNode>[];
for (final node in nodes) {
if (node.children.isEmpty) {
result.add(node);
} else {
result.addAll(_allLeaves(node.children));
}
}
return result;
}
}
class _InfoPanel extends StatelessWidget {
const _InfoPanel({
required this.title,
required this.value,
required this.icon,
required this.subtitle,
});
final String title;
final String value;
final IconData icon;
final String subtitle;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: Colors.white60),
),
const SizedBox(height: 6),
Text(
value,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.white70),
),
],
),
),
],
),
),
);
}
}
class _ActivityPanel extends StatelessWidget {
const _ActivityPanel({required this.activities});
final List<StudioActivity> activities;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最近活动',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
...activities.take(8).map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.bolt_rounded, size: 18),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: Theme.of(context).textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
item.detail,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.white70),
),
],
),
),
],
),
),
),
],
),
),
);
}
}
class _FeaturePanel extends StatelessWidget {
const _FeaturePanel({required this.onOpenNode});
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
final nodes = _allLeaves(studioNavTree);
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'功能覆盖',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
const Text(
'当前 Flutter 桌面端实现了 Studio 页面的核心交互:登录、导航、标签页、JSON/SQL 编辑、日志和运维任务。',
),
const SizedBox(height: 14),
Wrap(
spacing: 10,
runSpacing: 10,
children: nodes
.take(5)
.map(
(node) => FilledButton.tonal(
onPressed: () => onOpenNode(node),
child: Text(node.title),
),
)
.toList(),
),
],
),
),
);
}
List<StudioNavNode> _allLeaves(List<StudioNavNode> nodes) {
final result = <StudioNavNode>[];
for (final node in nodes) {
if (node.children.isEmpty) {
result.add(node);
} else {
result.addAll(_allLeaves(node.children));
}
}
return result;
}
}
+825
View File
@@ -0,0 +1,825 @@
part of 'studio_app.dart';
class StudioFormEditorPage extends StatefulWidget {
const StudioFormEditorPage({
super.key,
required this.tab,
required this.runtime,
required this.onAction,
});
final StudioTab tab;
final StudioRuntime runtime;
final void Function(String title, String detail) onAction;
@override
State<StudioFormEditorPage> createState() => _StudioFormEditorPageState();
}
class _StudioFormEditorPageState extends State<StudioFormEditorPage> {
late final TextEditingController _nameController;
late final TextEditingController _codeController;
late final TextEditingController _versionController;
late final TextEditingController _remarkController;
late final TextEditingController _contentController;
bool _loading = true;
bool _saving = false;
bool _loadedExisting = false;
String _status = '加载中';
String _detail = '';
List<Map<String, dynamic>> _options = <Map<String, dynamic>>[];
String _selectedKey = '';
bool get _isTableMode =>
widget.tab.id.contains('Table') || widget.tab.id.contains('table');
bool get _isBillMode =>
widget.tab.id.contains('Bill') || widget.tab.id.contains('bill');
bool get _isViewMode =>
widget.tab.id.contains('View') ||
widget.tab.id.contains('WsoData') ||
widget.tab.id.contains('view');
bool get _isNavigatorMode =>
widget.tab.id.contains('Navigator') ||
widget.tab.id.contains('Navigation') ||
widget.tab.id.contains('navigator') ||
widget.tab.id.contains('navigation');
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.tab.title);
_codeController = TextEditingController(text: widget.tab.id);
_versionController = TextEditingController(text: '1');
_remarkController = TextEditingController(text: widget.tab.summary);
_contentController = TextEditingController(text: '{}');
_loadInitialData();
}
@override
void dispose() {
_nameController.dispose();
_codeController.dispose();
_versionController.dispose();
_remarkController.dispose();
_contentController.dispose();
super.dispose();
}
Future<void> _loadInitialData() async {
if (_isTableMode) {
await _loadTableLikeOptions();
return;
}
if (_isBillMode) {
await _loadBillLikeOptions();
return;
}
if (_isViewMode) {
await _loadViewOptions();
return;
}
if (_isNavigatorMode) {
await _loadNavigator();
return;
}
setState(() {
_contentController.text = _prettyJson(_defaultPayload());
_status = '可直接编辑';
_loading = false;
});
}
Future<void> _loadTableLikeOptions() async {
try {
final items = await widget.runtime.client.fetchTableModels(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_options = items;
_selectedKey = items.isNotEmpty ? _itemKey(items.first) : '';
});
if (_selectedKey.isNotEmpty) {
await _loadTableLikeItem();
} else {
setState(() {
_loading = false;
_status = '没有找到数据模型';
_contentController.text = _prettyJson(_defaultPayload());
});
}
} catch (error, stackTrace) {
_reportError('加载数据模型失败', error, stackTrace);
}
}
Future<void> _loadBillLikeOptions() async {
try {
final items = await widget.runtime.client.fetchBillModels(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_options = items;
_selectedKey = items.isNotEmpty ? _itemKey(items.first) : '';
});
if (_selectedKey.isNotEmpty) {
await _loadBillLikeItem();
} else {
setState(() {
_loading = false;
_status = '没有找到单据模型';
_contentController.text = _prettyJson(_defaultPayload());
});
}
} catch (error, stackTrace) {
_reportError('加载单据模型失败', error, stackTrace);
}
}
Future<void> _loadViewOptions() async {
try {
final items = await widget.runtime.client.fetchViewModelOptions(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_options = items;
_selectedKey = items.isNotEmpty ? _viewItemKey(items.first) : '';
});
if (_selectedKey.isNotEmpty) {
await _loadViewItem();
} else {
setState(() {
_loading = false;
_status = '没有找到界面模型';
_contentController.text = _prettyJson(_defaultPayload());
});
}
} catch (error, stackTrace) {
_reportError('加载界面模型失败', error, stackTrace);
}
}
Future<void> _loadNavigator() async {
try {
final navigator = await widget.runtime.client.fetchCreationNavigatorRecord(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
final structure = navigator['structure'] is List
? List<Map<String, dynamic>>.from(
(navigator['structure'] as List).whereType<Map>(),
)
: <Map<String, dynamic>>[];
setState(() {
_nameController.text = widget.tab.title;
_codeController.text = widget.tab.id;
_versionController.text = _readValue(navigator, const ['version'], '1');
_remarkController.text = widget.tab.summary;
_contentController.text = _prettyJson(navigator.isNotEmpty
? navigator
: <String, dynamic>{'structure': structure});
_status = '导航数据已加载';
_detail = 'root items: ${structure.length}';
_loadedExisting = true;
_loading = false;
});
} catch (error, stackTrace) {
_reportError('加载导航失败', error, stackTrace);
}
}
Future<void> _loadTableLikeItem() async {
final code = _selectedKey;
if (code.isEmpty) {
return;
}
setState(() {
_loading = true;
_status = '正在加载数据模型...';
});
try {
final item = await widget.runtime.client.fetchTableModel(
code: code,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (item == null) {
setState(() {
_status = '模型不存在';
_detail = code;
_loading = false;
});
return;
}
_applyLoadedItem(item, code: _readValue(item, const ['code'], code));
} catch (error, stackTrace) {
_reportError('加载数据模型失败', error, stackTrace);
}
}
Future<void> _loadBillLikeItem() async {
final code = _selectedKey;
if (code.isEmpty) {
return;
}
setState(() {
_loading = true;
_status = '正在加载单据模型...';
});
try {
final item = await widget.runtime.client.fetchBillModel(
code: code,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (item == null) {
setState(() {
_status = '模型不存在';
_detail = code;
_loading = false;
});
return;
}
_applyLoadedItem(item, code: _readValue(item, const ['code'], code));
} catch (error, stackTrace) {
_reportError('加载单据模型失败', error, stackTrace);
}
}
Future<void> _loadViewItem() async {
final parts = _selectedKey.split('::');
if (parts.length != 2) {
return;
}
final tid = parts[0];
final oid = parts[1];
setState(() {
_loading = true;
_status = '正在加载界面模型...';
});
try {
final item = await widget.runtime.client.fetchViewModel(
tid: tid,
oid: oid,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (item == null) {
setState(() {
_status = '模型不存在';
_detail = '$tid / $oid';
_loading = false;
});
return;
}
_applyLoadedItem(
item,
code: _readValue(item, const ['tid', 'code'], tid),
version: _readValue(item, const ['oid', 'version'], oid),
);
} catch (error, stackTrace) {
_reportError('加载界面模型失败', error, stackTrace);
}
}
void _applyLoadedItem(
Map<String, dynamic> item, {
required String code,
String? version,
}) {
setState(() {
_nameController.text = _readValue(item, const ['name', 'title'], widget.tab.title);
_codeController.text = code;
_versionController.text = version ?? _readValue(item, const ['version', 'oid'], '1');
_remarkController.text = _readValue(item, const ['remark', 'description', 'summary'], widget.tab.summary);
_contentController.text = _prettyJson(item);
_status = '已加载';
_detail = code;
_loadedExisting = true;
_loading = false;
});
}
Map<String, dynamic> _defaultPayload() {
return <String, dynamic>{
'name': widget.tab.title,
'code': widget.tab.id,
'version': '1',
'remark': widget.tab.summary,
'enabled': true,
};
}
String _itemKey(Map<String, dynamic> item) {
return _readValue(item, const ['code', 'id']);
}
String _viewItemKey(Map<String, dynamic> item) {
final tid = _readValue(item, const ['tid']);
final oid = _readValue(item, const ['oid']);
if (tid.isEmpty || oid.isEmpty) {
return '';
}
return '$tid::$oid';
}
String _readValue(Map<String, dynamic> item, List<String> keys, [String fallback = '']) {
for (final key in keys) {
final value = item[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString();
}
}
return fallback;
}
String _prettyJson(dynamic value) {
try {
return const JsonEncoder.withIndent(' ').convert(value);
} catch (_) {
return value.toString();
}
}
void _reportError(String title, Object error, StackTrace stackTrace) {
debugPrint('========== $title ==========');
debugPrint('error: $error');
debugPrint('stackTrace:\n$stackTrace');
debugPrint('========== $title end ==========');
if (!mounted) {
return;
}
setState(() {
_status = '$title';
_detail = error.toString().replaceFirst('Bad state: ', '');
_loading = false;
});
}
Future<void> _save() async {
final parsed = _decodeContent();
if (parsed == null) {
setState(() {
_status = 'JSON 格式错误';
_detail = '请先修正内容后再保存';
});
return;
}
setState(() {
_saving = true;
_status = _loadedExisting ? '正在更新...' : '正在创建...';
_detail = '';
});
try {
final wasExisting = _loadedExisting;
final result = await _saveByMode(parsed);
if (!mounted) {
return;
}
setState(() {
_loadedExisting = true;
_status = result.success ? '保存成功' : '保存失败';
_detail = _prettyJson(result.raw);
});
widget.onAction(wasExisting ? '更新' : '创建', widget.tab.title);
} catch (error, stackTrace) {
_reportError('保存失败', error, stackTrace);
} finally {
if (mounted) {
setState(() {
_saving = false;
});
}
}
}
Future<StudioBackendActionResult> _saveByMode(dynamic parsed) async {
if (_isNavigatorMode) {
final map = parsed is Map<String, dynamic>
? Map<String, dynamic>.from(parsed)
: <String, dynamic>{'structure': parsed};
final structure = map['structure'] is List
? List<Map<String, dynamic>>.from(
(map['structure'] as List).whereType<Map>(),
)
: <Map<String, dynamic>>[];
final version = int.tryParse(_versionController.text.trim()) ?? 1;
return widget.runtime.client.saveCreationNavigator(
data: structure,
version: version,
session: widget.runtime.session,
);
}
final payload = parsed is Map<String, dynamic>
? Map<String, dynamic>.from(parsed)
: <String, dynamic>{'data': parsed};
payload['name'] = _nameController.text.trim();
payload['code'] = _codeController.text.trim();
payload['version'] = _versionController.text.trim();
payload['remark'] = _remarkController.text.trim();
if (_isViewMode) {
payload['tid'] = _codeController.text.trim();
payload['oid'] = _versionController.text.trim();
}
if (_isTableMode) {
return _loadedExisting
? widget.runtime.client.updateTableModel(
payload: payload,
session: widget.runtime.session,
)
: widget.runtime.client.createTableModel(
payload: payload,
session: widget.runtime.session,
);
}
if (_isBillMode) {
return _loadedExisting
? widget.runtime.client.updateBillModel(
payload: payload,
session: widget.runtime.session,
)
: widget.runtime.client.createBillModel(
payload: payload,
session: widget.runtime.session,
);
}
if (_isViewMode) {
return _loadedExisting
? widget.runtime.client.updateViewModel(
payload: payload,
session: widget.runtime.session,
)
: widget.runtime.client.createViewModel(
payload: payload,
session: widget.runtime.session,
);
}
return StudioBackendActionResult(
success: true,
code: '',
message: '无需保存',
data: payload,
raw: payload,
);
}
dynamic _decodeContent() {
try {
return jsonDecode(_contentController.text);
} catch (_) {
return null;
}
}
void _formatContent() {
final parsed = _decodeContent();
if (parsed == null) {
setState(() {
_status = 'JSON 格式错误,无法格式化';
});
return;
}
setState(() {
_contentController.text = _prettyJson(parsed);
_status = 'JSON 已格式化';
});
widget.onAction('格式化 JSON', widget.tab.title);
}
Future<void> _reload() async {
if (_isTableMode) {
await _loadTableLikeItem();
return;
}
if (_isBillMode) {
await _loadBillLikeItem();
return;
}
if (_isViewMode) {
await _loadViewItem();
return;
}
if (_isNavigatorMode) {
await _loadNavigator();
return;
}
}
Future<void> _delete() async {
if (!_loadedExisting) {
setState(() {
_status = '当前内容还没有保存过,无需删除';
});
return;
}
try {
StudioBackendActionResult result;
if (_isTableMode) {
result = await widget.runtime.client.deleteTableModel(
code: _codeController.text.trim(),
session: widget.runtime.session,
);
} else if (_isBillMode) {
result = await widget.runtime.client.deleteBillModel(
code: _codeController.text.trim(),
session: widget.runtime.session,
);
} else if (_isViewMode) {
result = await widget.runtime.client.deleteViewModel(
tid: _codeController.text.trim(),
oid: _versionController.text.trim(),
session: widget.runtime.session,
);
} else {
setState(() {
_status = '当前页面不支持删除';
});
return;
}
if (!mounted) {
return;
}
setState(() {
_status = result.success ? '删除成功' : '删除失败';
_detail = _prettyJson(result.raw);
});
widget.onAction('删除', widget.tab.title);
} catch (error, stackTrace) {
_reportError('删除失败', error, stackTrace);
}
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 980;
final leftPanel = _buildEditorPanel(context);
final rightPanel = _buildPreviewPanel(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (wide)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: leftPanel),
const SizedBox(width: 16),
Expanded(child: rightPanel),
],
)
else ...[
leftPanel,
const SizedBox(height: 16),
rightPanel,
],
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
Expanded(
child: SelectableText(
_status,
style: Theme.of(context).textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.w700),
),
),
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _reload,
icon: const Icon(Icons.refresh_rounded),
label: const Text('重新加载'),
),
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _formatContent,
icon: const Icon(Icons.data_object_rounded),
label: const Text('格式化'),
),
const SizedBox(width: 10),
FilledButton.icon(
onPressed: _loading || _saving ? null : _save,
icon: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_rounded),
label: Text(_loadedExisting ? '更新' : '保存'),
),
const SizedBox(width: 10),
if (!_isNavigatorMode)
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _delete,
icon: const Icon(Icons.delete_rounded),
label: const Text('删除'),
),
],
),
),
),
],
);
},
),
);
}
Widget _buildEditorPanel(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'属性编辑',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 14),
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '名称',
prefixIcon: Icon(Icons.title_rounded),
),
),
const SizedBox(height: 12),
TextField(
controller: _codeController,
decoration: InputDecoration(
labelText: _isViewMode ? 'tid' : '编码',
prefixIcon: const Icon(Icons.code_rounded),
),
),
const SizedBox(height: 12),
TextField(
controller: _versionController,
decoration: InputDecoration(
labelText: _isViewMode ? 'oid' : '版本',
prefixIcon: const Icon(Icons.sell_rounded),
),
),
const SizedBox(height: 12),
TextField(
controller: _remarkController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '说明',
alignLabelWithHint: true,
prefixIcon: Icon(Icons.notes_rounded),
),
),
const SizedBox(height: 14),
if (_options.isNotEmpty && !_isNavigatorMode) ...[
DropdownButtonFormField<String>(
value: _selectedKey.isNotEmpty ? _selectedKey : null,
decoration: InputDecoration(
labelText: _isViewMode ? '选择 tid / oid' : '选择模型',
),
items: _options.map((item) {
final value = _isViewMode ? _viewItemKey(item) : _itemKey(item);
final label = _isViewMode
? '${_readValue(item, const ['tid'])} / ${_readValue(item, const ['oid'])}'
: _readValue(item, const ['code', 'id']);
return DropdownMenuItem<String>(
value: value,
child: Text(label),
);
}).toList(),
onChanged: (value) {
if (value == null || value.isEmpty) {
return;
}
setState(() {
_selectedKey = value;
});
_reload();
},
),
const SizedBox(height: 12),
],
Text(
'JSON 内容',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 10),
SizedBox(
height: 360,
child: TextField(
controller: _contentController,
expands: true,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(fontFamily: 'monospace'),
decoration: const InputDecoration(
hintText: '编辑或粘贴 JSON',
alignLabelWithHint: true,
prefixIcon: Icon(Icons.description_rounded),
),
),
),
],
),
),
);
}
Widget _buildPreviewPanel(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'预览与原文',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _softPanelColor(context),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: SelectableText(
_prettyJson(_decodeContent() ?? _currentPreview()),
style: const TextStyle(fontFamily: 'monospace', height: 1.5),
),
),
const SizedBox(height: 14),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
_MiniBadge(text: widget.tab.pageType.name),
_MiniBadge(text: widget.tab.id),
_MiniBadge(text: _loadedExisting ? '已加载' : '新建'),
],
),
const SizedBox(height: 14),
SelectableText(
_detail.isEmpty ? '暂无详情' : _detail,
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
],
),
),
);
}
dynamic _currentPreview() {
if (_isNavigatorMode) {
return <String, dynamic>{
'structure': <Map<String, dynamic>>[],
'version': _versionController.text.trim(),
};
}
return <String, dynamic>{
'name': _nameController.text.trim(),
'code': _codeController.text.trim(),
'version': _versionController.text.trim(),
'remark': _remarkController.text.trim(),
};
}
}
+192
View File
@@ -0,0 +1,192 @@
part of 'studio_app.dart';
class StudioHistoryPage extends StatefulWidget {
const StudioHistoryPage({
super.key,
required this.tab,
required this.runtime,
required this.onAction,
});
final StudioTab tab;
final StudioRuntime runtime;
final void Function(String title, String detail) onAction;
@override
State<StudioHistoryPage> createState() => _StudioHistoryPageState();
}
class _StudioHistoryPageState extends State<StudioHistoryPage> {
bool _loading = true;
String? _errorMessage;
List<Map<String, dynamic>> _rows = <Map<String, dynamic>>[];
@override
void initState() {
super.initState();
_reload();
}
Future<void> _reload() async {
setState(() {
_loading = true;
_errorMessage = null;
});
try {
final rows = await widget.runtime.client.fetchUpgradeLogs(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_rows = rows;
});
} catch (error, stackTrace) {
_printError('加载历史失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_errorMessage = _normalizeError(error);
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
void _printError(String title, Object error, StackTrace stackTrace) {
debugPrint('========== $title ==========');
debugPrint('error: $error');
debugPrint('stackTrace:\n$stackTrace');
debugPrint('========== $title end ==========');
}
String _normalizeError(Object error) {
final text = error.toString();
const marker = 'Bad state: ';
if (text.startsWith(marker)) {
return text.substring(marker.length);
}
return text;
}
String _value(Map<String, dynamic> row, List<String> keys, [String fallback = '']) {
for (final key in keys) {
final value = row[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString();
}
}
return fallback;
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
'从后端读取发布与修改历史。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
],
),
),
FilledButton.tonalIcon(
onPressed: _loading ? null : _reload,
icon: _loading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh_rounded),
label: const Text('刷新'),
),
],
),
const SizedBox(height: 12),
if (_errorMessage != null) ...[
SelectableText(
_errorMessage!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 12),
],
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
child: Center(child: CircularProgressIndicator()),
)
else
SizedBox(
width: double.infinity,
child: DataTable(
columns: const [
DataColumn(label: Text('版本')),
DataColumn(label: Text('操作者')),
DataColumn(label: Text('时间')),
DataColumn(label: Text('状态')),
DataColumn(label: Text('说明')),
],
rows: _rows
.map(
(row) => DataRow(
cells: [
DataCell(Text(_value(row, const ['version', 'tag', 'name'], ''))),
DataCell(Text(_value(row, const ['user', 'operator', 'creator'], ''))),
DataCell(Text(_value(row, const ['time', 'createTime', 'date'], ''))),
DataCell(Text(_value(row, const ['status', 'state'], ''))),
DataCell(
SelectableText(
_value(
row,
const ['note', 'msg', 'message', 'summary', 'desc'],
row.toString(),
),
),
),
],
),
)
.toList(),
),
),
const SizedBox(height: 8),
Text(
'${_rows.length} 条历史记录',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.62)),
),
],
),
),
),
);
}
}
+266
View File
@@ -0,0 +1,266 @@
part of 'studio_app.dart';
class StudioOperationsPage extends StatefulWidget {
const StudioOperationsPage({
super.key,
required this.tab,
required this.runtime,
required this.onAction,
});
final StudioTab tab;
final StudioRuntime runtime;
final void Function(String title, String detail) onAction;
@override
State<StudioOperationsPage> createState() => _StudioOperationsPageState();
}
class _StudioOperationsPageState extends State<StudioOperationsPage> {
bool _loading = false;
String _status = '等待执行';
String _detail = '';
Map<String, dynamic>? _lastData;
Future<void> _downloadConfigs() async {
setState(() {
_loading = true;
_status = '正在下载配置包...';
_detail = '';
_lastData = null;
});
try {
final result = await widget.runtime.client.downloadModelConfigs(
session: widget.runtime.session,
);
final savedPath = await saveBytesToFile(
bytes: result.bytes,
suggestedName: result.filename,
);
if (!mounted) {
return;
}
setState(() {
_status = savedPath == null
? '已下载配置包,但未保存'
: '配置包已保存';
_detail = savedPath == null
? '文件名:${result.filename}\n大小:${result.bytes.length} bytes'
: '文件:$savedPath\n文件名:${result.filename}\n大小:${result.bytes.length} bytes';
_lastData = <String, dynamic>{
'filename': result.filename,
'contentType': result.contentType,
'statusCode': result.statusCode,
'savedPath': savedPath,
'size': result.bytes.length,
};
});
widget.onAction('下载配置', result.filename);
} catch (error, stackTrace) {
_printError('下载配置失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_status = '下载失败';
_detail = _normalizeError(error);
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
Future<void> _repairDepartment() async {
setState(() {
_loading = true;
_status = '正在执行部门修复...';
_detail = '';
_lastData = null;
});
try {
final result = await widget.runtime.client.repairDepartment(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_status = result.success ? '修复完成' : '修复失败';
_detail = _formatActionResult(result);
_lastData = result.raw;
});
widget.onAction('部门修复', result.message.isNotEmpty ? result.message : '已执行');
} catch (error, stackTrace) {
_printError('部门修复失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_status = '修复失败';
_detail = _normalizeError(error);
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
void _printError(String title, Object error, StackTrace stackTrace) {
debugPrint('========== $title ==========');
debugPrint('error: $error');
debugPrint('stackTrace:\n$stackTrace');
debugPrint('========== $title end ==========');
}
String _normalizeError(Object error) {
final text = error.toString();
const marker = 'Bad state: ';
if (text.startsWith(marker)) {
return text.substring(marker.length);
}
return text;
}
String _formatActionResult(StudioBackendActionResult result) {
final buffer = StringBuffer()
..writeln('code: ${result.code.isEmpty ? '' : result.code}')
..writeln('message: ${result.message.isEmpty ? '' : result.message}')
..writeln('data: ${const JsonEncoder.withIndent(' ').convert(result.data)}')
..writeln('raw: ${const JsonEncoder.withIndent(' ').convert(result.raw)}');
return buffer.toString();
}
@override
Widget build(BuildContext context) {
final isRepair = widget.tab.pageType == StudioPageType.repair;
final primaryAction = isRepair ? _repairDepartment : _downloadConfigs;
final primaryLabel = isRepair ? '执行修复' : '下载配置';
final primaryIcon = isRepair ? Icons.build_rounded : Icons.download_rounded;
final secondaryHint = isRepair
? '部门修复会直接调用后端接口,执行后请刷新相关页面确认数据。'
: '下载配置会调用后端压缩包接口,并让你选择本地保存位置。';
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
widget.tab.summary,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
const SizedBox(height: 16),
LinearProgressIndicator(
value: _loading ? null : 1.0,
minHeight: 10,
borderRadius: BorderRadius.circular(999),
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'操作说明',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(secondaryHint),
const SizedBox(height: 14),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
FilledButton.icon(
onPressed: _loading ? null : primaryAction,
icon: _loading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(primaryIcon),
label: Text(primaryLabel),
),
FilledButton.tonalIcon(
onPressed: _loading
? null
: () => widget.onAction('记录动作', widget.tab.title),
icon: const Icon(Icons.history_rounded),
label: const Text('记录动作'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'执行结果',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 10),
SelectableText(
_status,
style: Theme.of(context).textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.w700),
),
if (_detail.isNotEmpty) ...[
const SizedBox(height: 10),
SelectableText(
_detail,
style: Theme.of(context).textTheme.bodyMedium,
),
],
if (_lastData != null) ...[
const SizedBox(height: 12),
SelectableText(
const JsonEncoder.withIndent(' ').convert(_lastData),
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
],
],
),
),
),
],
),
);
}
}
+150
View File
@@ -0,0 +1,150 @@
part of 'studio_app.dart';
class StudioPortalMaintainPage extends StatefulWidget {
const StudioPortalMaintainPage({
super.key,
required this.tab,
required this.runtime,
required this.onAction,
});
final StudioTab tab;
final StudioRuntime runtime;
final void Function(String title, String detail) onAction;
@override
State<StudioPortalMaintainPage> createState() => _StudioPortalMaintainPageState();
}
class _StudioPortalMaintainPageState extends State<StudioPortalMaintainPage> {
bool _loading = true;
String _status = '正在加载门户';
List<Map<String, dynamic>> _rows = <Map<String, dynamic>>[];
@override
void initState() {
super.initState();
_reload();
}
Future<void> _reload() async {
setState(() {
_loading = true;
_status = '正在加载门户';
});
try {
final rows = await widget.runtime.client.fetchPortalConfigs(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_rows = rows;
_status = '门户已加载';
});
widget.onAction('加载门户', widget.tab.title);
} catch (error, stackTrace) {
debugPrint('========== 加载门户失败 ==========');
debugPrint('error: $error');
debugPrint('stackTrace:\n$stackTrace');
debugPrint('========== 加载门户失败 end ==========');
if (!mounted) {
return;
}
setState(() {
_status = error.toString();
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
String _value(Map<String, dynamic> row, List<String> keys, [String fallback = '']) {
for (final key in keys) {
final value = row[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString();
}
}
return fallback;
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
'读取后端 portal 配置列表。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
],
),
),
FilledButton.tonalIcon(
onPressed: _loading ? null : _reload,
icon: const Icon(Icons.refresh_rounded),
label: const Text('刷新'),
),
],
),
const SizedBox(height: 12),
SelectableText(_status),
const SizedBox(height: 12),
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(child: CircularProgressIndicator()),
)
else
DataTable(
columns: const [
DataColumn(label: Text('编码')),
DataColumn(label: Text('名称')),
DataColumn(label: Text('描述')),
],
rows: _rows
.map(
(row) => DataRow(
cells: [
DataCell(Text(_value(row, const ['id', 'code']))),
DataCell(Text(_value(row, const ['label', 'name']))),
DataCell(
SelectableText(
_value(row, const ['remark', 'description', 'portal']),
),
),
],
),
)
.toList(),
),
],
),
),
),
);
}
}
+662
View File
@@ -0,0 +1,662 @@
part of 'studio_app.dart';
class StudioSqlPage extends StatefulWidget {
const StudioSqlPage({
super.key,
required this.tab,
required this.runtime,
required this.onAction,
});
final StudioTab tab;
final StudioRuntime runtime;
final void Function(String title, String detail) onAction;
@override
State<StudioSqlPage> createState() => _StudioSqlPageState();
}
class _StudioSqlPageState extends State<StudioSqlPage> {
late final TextEditingController _sqlController;
late final TextEditingController _limitController;
late final TextEditingController _countSqlController;
late final TextEditingController _summarySqlController;
late final TextEditingController _criteriaController;
late final TextEditingController _extConfigController;
bool _loading = true;
bool _templateLoaded = false;
String _status = '等待加载';
String _detail = '';
Map<String, List<String>> _templateIndex = <String, List<String>>{};
List<Map<String, dynamic>> _resultRows = <Map<String, dynamic>>[];
List<String> _resultColumns = <String>[];
String _selectedTidValue = '';
String _selectedOidValue = '';
bool get _isTemplateMode => widget.tab.pageType == StudioPageType.sqlTemplate;
@override
void initState() {
super.initState();
_sqlController = TextEditingController(
text:
'select id, code, name from ttx_demo_table order by id desc limit 20;',
);
_limitController = TextEditingController(text: '100');
_countSqlController = TextEditingController();
_summarySqlController = TextEditingController();
_criteriaController = TextEditingController(text: '{}');
_extConfigController = TextEditingController(text: '{}');
if (_isTemplateMode) {
_loadTemplateIndex();
} else {
_loading = false;
_status = '可直接执行 SQL';
}
}
@override
void dispose() {
_sqlController.dispose();
_limitController.dispose();
_countSqlController.dispose();
_summarySqlController.dispose();
_criteriaController.dispose();
_extConfigController.dispose();
super.dispose();
}
Future<void> _loadTemplateIndex() async {
try {
final items = await widget.runtime.client.fetchSqlTemplateIndex(
session: widget.runtime.session,
);
final index = <String, List<String>>{};
for (final item in items) {
final tid = _readValue(item, const ['tid']);
final oid = _readValue(item, const ['oid']);
if (tid.isEmpty || oid.isEmpty) {
continue;
}
index.putIfAbsent(tid, () => <String>[]).add(oid);
}
if (!mounted) {
return;
}
setState(() {
_templateIndex = index;
_status = index.isEmpty ? '没有找到 SQL 模板' : '已加载模板索引';
_loading = false;
});
if (index.isNotEmpty) {
await _loadSelectedTemplate();
}
} catch (error, stackTrace) {
_printError('加载 SQL 模板索引失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_status = '加载模板失败';
_detail = _normalizeError(error);
_loading = false;
});
}
}
String _selectedTid() {
if (_selectedTidValue.isNotEmpty) {
return _selectedTidValue;
}
if (_templateIndex.isEmpty) {
return '';
}
return _templateIndex.keys.first;
}
String _selectedOid([String? tid]) {
final selectedTid = tid ?? _selectedTid();
final oids = _templateIndex[selectedTid];
if (oids == null || oids.isEmpty) {
return '';
}
if (_selectedOidValue.isNotEmpty && oids.contains(_selectedOidValue)) {
return _selectedOidValue;
}
return oids.first;
}
Future<void> _loadSelectedTemplate() async {
final tid = _selectedTid();
final oid = _selectedOid(tid);
if (tid.isEmpty || oid.isEmpty) {
return;
}
setState(() {
_loading = true;
_status = '正在加载模板...';
});
try {
final template = await widget.runtime.client.fetchSqlTemplate(
tid: tid,
oid: oid,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (template == null) {
setState(() {
_templateLoaded = false;
_status = '模板不存在';
_detail = '$tid / $oid';
});
return;
}
setState(() {
_templateLoaded = true;
_sqlController.text = _readValue(template, const ['value', 'sql']);
_countSqlController.text = _readValue(template, const ['countSql', 'count']);
_summarySqlController.text = _readValue(template, const ['summarySql', 'summary']);
_criteriaController.text = _readValue(template, const ['criteria'], '{}');
_extConfigController.text = _readValue(template, const ['extConfig'], '{}');
_status = '模板已加载';
_detail = '$tid / $oid';
});
widget.onAction('加载模板', '$tid / $oid');
} catch (error, stackTrace) {
_printError('加载 SQL 模板失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_templateLoaded = false;
_status = '加载模板失败';
_detail = _normalizeError(error);
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
Future<void> _runSql() async {
setState(() {
_loading = true;
_status = '正在执行 SQL...';
_detail = '';
_resultRows = <Map<String, dynamic>>[];
_resultColumns = <String>[];
});
try {
final limit = int.tryParse(_limitController.text.trim()) ?? 100;
final result = await widget.runtime.client.runSql(
sql: _sqlController.text,
limit: limit,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
final data = result.data is Map
? Map<String, dynamic>.from(result.data as Map)
: <String, dynamic>{};
final rows = _extractRows(data['list'] ?? data['rows'] ?? data['data']);
setState(() {
_resultRows = rows;
_resultColumns = rows.isEmpty ? <String>[] : rows.first.keys.toList();
_status = result.success ? 'SQL 执行完成' : 'SQL 执行失败';
_detail = _formatActionResult(result);
});
widget.onAction('执行 SQL', widget.tab.title);
} catch (error, stackTrace) {
_printError('执行 SQL 失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_status = 'SQL 执行失败';
_detail = _normalizeError(error);
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
Future<void> _saveTemplate() async {
final tid = _selectedTid();
final oid = _selectedOid(tid);
if (tid.isEmpty || oid.isEmpty) {
setState(() {
_status = '请选择 tid 和 oid';
});
return;
}
setState(() {
_loading = true;
_status = _templateLoaded ? '正在更新模板...' : '正在创建模板...';
_detail = '';
});
try {
final payload = <String, dynamic>{
'value': _sqlController.text,
'count': _countSqlController.text,
'criteria': _criteriaController.text,
'summary': _summarySqlController.text,
'extConfig': _extConfigController.text,
};
final wasLoaded = _templateLoaded;
final result = _templateLoaded
? await widget.runtime.client.updateSqlTemplate(
tid: tid,
oid: oid,
payload: payload,
session: widget.runtime.session,
)
: await widget.runtime.client.createSqlTemplate(
tid: tid,
oid: oid,
payload: payload,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_templateLoaded = true;
_status = result.success ? '模板已保存' : '保存失败';
_detail = _formatActionResult(result);
});
widget.onAction(wasLoaded ? '更新模板' : '创建模板', '$tid / $oid');
} catch (error, stackTrace) {
_printError('保存 SQL 模板失败', error, stackTrace);
if (!mounted) {
return;
}
setState(() {
_status = '保存失败';
_detail = _normalizeError(error);
});
} finally {
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
void _formatSql() {
setState(() {
_sqlController.text = _sqlController.text
.replaceAll(RegExp(r'\s+'), ' ')
.trim();
});
widget.onAction('格式化 SQL', widget.tab.title);
}
void _printError(String title, Object error, StackTrace stackTrace) {
debugPrint('========== $title ==========');
debugPrint('error: $error');
debugPrint('stackTrace:\n$stackTrace');
debugPrint('========== $title end ==========');
}
String _normalizeError(Object error) {
final text = error.toString();
const marker = 'Bad state: ';
if (text.startsWith(marker)) {
return text.substring(marker.length);
}
return text;
}
String _readValue(Map<String, dynamic> map, List<String> keys, [String fallback = '']) {
for (final key in keys) {
final value = map[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString();
}
}
return fallback;
}
List<Map<String, dynamic>> _extractRows(dynamic value) {
if (value is List) {
return value
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
return <Map<String, dynamic>>[];
}
String _formatActionResult(StudioBackendActionResult result) {
return const JsonEncoder.withIndent(' ').convert({
'code': result.code,
'message': result.message,
'data': result.data,
'raw': result.raw,
});
}
Widget _buildTemplateBody(BuildContext context) {
final tids = _templateIndex.keys.toList();
final selectedTid = _selectedTid();
final oids = selectedTid.isEmpty
? const <String>[]
: _templateIndex[selectedTid] ?? const <String>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Wrap(
spacing: 12,
runSpacing: 12,
children: [
SizedBox(
width: 260,
child: DropdownButtonFormField<String>(
value: tids.contains(selectedTid) ? selectedTid : null,
decoration: const InputDecoration(labelText: 'tid'),
items: tids
.map((tid) => DropdownMenuItem(value: tid, child: Text(tid)))
.toList(),
onChanged: (value) {
if (value == null) {
return;
}
setState(() {
_selectedTidValue = value;
_selectedOidValue = (_templateIndex[value] ?? const <String>[]).isNotEmpty
? _templateIndex[value]!.first
: '';
});
},
),
),
SizedBox(
width: 260,
child: DropdownButtonFormField<String>(
value: oids.contains(_selectedOid()) ? _selectedOid() : null,
decoration: const InputDecoration(labelText: 'oid'),
items: oids
.map((oid) => DropdownMenuItem(value: oid, child: Text(oid)))
.toList(),
onChanged: (value) {
if (value == null) {
return;
}
setState(() {
_selectedOidValue = value;
});
},
),
),
FilledButton.tonalIcon(
onPressed: _loading ? null : _loadSelectedTemplate,
icon: const Icon(Icons.folder_open_rounded),
label: const Text('加载模板'),
),
FilledButton.icon(
onPressed: _loading ? null : _saveTemplate,
icon: const Icon(Icons.save_rounded),
label: Text(_templateLoaded ? '更新模板' : '创建模板'),
),
],
),
const SizedBox(height: 14),
_buildSqlEditors(context, includeTemplateFields: true),
],
);
}
@override
Widget build(BuildContext context) {
if (_isTemplateMode && _selectedTidValue.isEmpty && _templateIndex.isNotEmpty) {
_selectedTidValue = _templateIndex.keys.first;
_selectedOidValue = _templateIndex[_selectedTidValue]!.first;
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
_isTemplateMode ? '从后端加载 SQL 模板并支持创建、更新。' : '直接执行 SQL 并查看结果。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
],
),
),
),
const SizedBox(height: 16),
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(child: CircularProgressIndicator()),
)
else if (_isTemplateMode)
_buildTemplateBody(context)
else
_buildSqlEditors(context, includeTemplateFields: false),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'执行状态',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
),
if (!_isTemplateMode)
FilledButton.tonalIcon(
onPressed: _loading ? null : _runSql,
icon: const Icon(Icons.play_arrow_rounded),
label: const Text('执行 SQL'),
),
if (!_isTemplateMode) const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _formatSql,
icon: const Icon(Icons.format_align_left_rounded),
label: const Text('格式化'),
),
if (_isTemplateMode) ...[
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _saveTemplate,
icon: const Icon(Icons.save_rounded),
label: const Text('保存模板'),
),
],
],
),
const SizedBox(height: 10),
SelectableText(_status),
if (_detail.isNotEmpty) ...[
const SizedBox(height: 10),
SelectableText(
_detail,
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
],
],
),
),
),
if (_resultRows.isNotEmpty) ...[
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'执行结果',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: _resultColumns
.map((column) => DataColumn(label: Text(column)))
.toList(),
rows: _resultRows
.map(
(row) => DataRow(
cells: _resultColumns
.map(
(column) => DataCell(
SelectableText(_formatCellValue(row[column])),
),
)
.toList(),
),
)
.toList(),
),
),
],
),
),
),
],
],
),
);
}
Widget _buildSqlEditors(BuildContext context, {required bool includeTemplateFields}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (includeTemplateFields) ...[
Wrap(
spacing: 12,
runSpacing: 12,
children: [
SizedBox(
width: 220,
child: TextField(
controller: _countSqlController,
decoration: const InputDecoration(labelText: 'count SQL'),
maxLines: 3,
),
),
SizedBox(
width: 220,
child: TextField(
controller: _summarySqlController,
decoration: const InputDecoration(labelText: 'summary SQL'),
maxLines: 3,
),
),
],
),
const SizedBox(height: 12),
TextField(
controller: _criteriaController,
decoration: const InputDecoration(
labelText: 'criteria',
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 12),
TextField(
controller: _extConfigController,
decoration: const InputDecoration(
labelText: 'extConfig',
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 14),
],
SizedBox(
height: 280,
child: TextField(
controller: _sqlController,
expands: true,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(fontFamily: 'monospace'),
decoration: const InputDecoration(
labelText: 'SQL 内容',
alignLabelWithHint: true,
),
),
),
if (!includeTemplateFields) ...[
const SizedBox(height: 12),
Row(
children: [
SizedBox(
width: 160,
child: TextField(
controller: _limitController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'limit'),
),
),
const SizedBox(width: 12),
FilledButton.tonalIcon(
onPressed: _loading ? null : _formatSql,
icon: const Icon(Icons.format_align_left_rounded),
label: const Text('格式化'),
),
],
),
],
],
),
),
);
}
String _formatCellValue(Object? value) {
if (value == null) {
return '';
}
if (value is Map || value is List) {
return const JsonEncoder.withIndent(' ').convert(value);
}
return value.toString();
}
}
+268
View File
@@ -0,0 +1,268 @@
part of 'studio_app.dart';
class StudioShell extends StatefulWidget {
const StudioShell({
super.key,
required this.runtime,
required this.languageCode,
required this.themeMode,
required this.onToggleTheme,
required this.onLogout,
});
final StudioRuntime runtime;
final String languageCode;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final VoidCallback onLogout;
@override
State<StudioShell> createState() => _StudioShellState();
}
class _StudioShellState extends State<StudioShell> {
final List<StudioActivity> _activities = <StudioActivity>[];
final List<StudioTab> _tabs = <StudioTab>[StudioTab.dashboard()];
String _searchQuery = '';
String _selectedTabId = StudioTab.dashboardId;
int _refreshRevision = 0;
StudioUser get user => widget.runtime.user;
@override
void initState() {
super.initState();
_activities.add(
StudioActivity(
title: '登录成功',
detail: '${user.displayName} / ${user.workspace}',
),
);
}
StudioTab get _selectedTab =>
_tabs.firstWhere((tab) => tab.id == _selectedTabId);
void _log(String title, String detail) {
setState(() {
_activities.insert(0, StudioActivity(title: title, detail: detail));
if (_activities.length > 24) {
_activities.removeLast();
}
});
}
void _openNode(StudioNavNode node) {
final existingIndex = _tabs.indexWhere((tab) => tab.id == node.id);
setState(() {
if (existingIndex == -1) {
_tabs.add(
StudioTab(
id: node.id,
title: node.title,
summary: node.summary,
pageType: node.pageType,
icon: node.icon,
),
);
}
_selectedTabId = node.id;
});
_log('打开页面', node.title);
}
void _selectTab(String tabId) {
setState(() {
_selectedTabId = tabId;
});
}
void _closeTab(String tabId) {
if (tabId == StudioTab.dashboardId) {
return;
}
setState(() {
_tabs.removeWhere((tab) => tab.id == tabId);
if (_selectedTabId == tabId) {
_selectedTabId = _tabs.isNotEmpty
? _tabs.last.id
: StudioTab.dashboardId;
}
});
_log('关闭页面', tabId);
}
void _refreshPage() {
setState(() {
_refreshRevision += 1;
});
_log('刷新页面', _selectedTab.title);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('已刷新 ${_selectedTab.title}')));
}
void _handlePageAction(String title, String detail) {
_log(title, detail);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(detail)));
}
List<StudioNavNode> _allLeaves(List<StudioNavNode> nodes) {
final result = <StudioNavNode>[];
for (final node in nodes) {
if (node.children.isEmpty) {
result.add(node);
} else {
result.addAll(_allLeaves(node.children));
}
}
return result;
}
@override
Widget build(BuildContext context) {
final wide = MediaQuery.of(context).size.width >= 1080;
final visibleRoots = _filterNavNodes(
studioNavTree,
widget.runtime.allowedMenuIds,
);
final leaves = _allLeaves(visibleRoots);
final filteredLeaves = _searchQuery.trim().isEmpty
? leaves
: leaves
.where(
(node) =>
node.title.contains(_searchQuery) ||
node.summary.contains(_searchQuery),
)
.toList();
return Scaffold(
appBar: wide
? null
: AppBar(
title: const Text('TTX Studio'),
actions: [
IconButton(
tooltip: widget.themeMode == ThemeMode.dark
? '切换亮色主题'
: '切换暗色主题',
onPressed: widget.onToggleTheme,
icon: Icon(
widget.themeMode == ThemeMode.dark
? Icons.light_mode_rounded
: Icons.dark_mode_rounded,
),
),
IconButton(
tooltip: '刷新',
onPressed: _refreshPage,
icon: const Icon(Icons.refresh_rounded),
),
IconButton(
tooltip: '退出登录',
onPressed: widget.onLogout,
icon: const Icon(Icons.logout_rounded),
),
],
),
drawer: wide
? null
: Drawer(
child: SafeArea(
child: StudioSidebar(
user: user,
themeMode: widget.themeMode,
onToggleTheme: widget.onToggleTheme,
searchQuery: _searchQuery,
filteredLeaves: filteredLeaves,
rootNodes: visibleRoots,
onSearchChanged: (value) {
setState(() {
_searchQuery = value;
});
},
onOpenNode: (node) {
Navigator.of(context).pop();
_openNode(node);
},
onLogout: widget.onLogout,
),
),
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _shellGradientColors(context),
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: wide
? Row(
children: [
SizedBox(
width: 336,
child: StudioSidebar(
user: user,
themeMode: widget.themeMode,
onToggleTheme: widget.onToggleTheme,
searchQuery: _searchQuery,
filteredLeaves: filteredLeaves,
rootNodes: visibleRoots,
onSearchChanged: (value) {
setState(() {
_searchQuery = value;
});
},
onOpenNode: _openNode,
onLogout: widget.onLogout,
),
),
Expanded(
child: StudioWorkspaceView(
runtime: widget.runtime,
user: user,
selectedTab: _selectedTab,
tabs: List<StudioTab>.unmodifiable(_tabs),
activities: List<StudioActivity>.unmodifiable(
_activities,
),
rootNodes: visibleRoots,
refreshToken: _refreshRevision,
onSelectTab: _selectTab,
onCloseTab: _closeTab,
onRefresh: _refreshPage,
onLogout: widget.onLogout,
themeMode: widget.themeMode,
onToggleTheme: widget.onToggleTheme,
onPageAction: _handlePageAction,
onOpenNode: _openNode,
),
),
],
)
: StudioWorkspaceView(
runtime: widget.runtime,
user: user,
selectedTab: _selectedTab,
tabs: List<StudioTab>.unmodifiable(_tabs),
activities: List<StudioActivity>.unmodifiable(_activities),
rootNodes: visibleRoots,
refreshToken: _refreshRevision,
onSelectTab: _selectTab,
onCloseTab: _closeTab,
onRefresh: _refreshPage,
onLogout: widget.onLogout,
themeMode: widget.themeMode,
onToggleTheme: widget.onToggleTheme,
onPageAction: _handlePageAction,
onOpenNode: _openNode,
),
),
),
);
}
}
+309
View File
@@ -0,0 +1,309 @@
part of 'studio_app.dart';
class StudioSidebar extends StatelessWidget {
const StudioSidebar({
super.key,
required this.user,
required this.themeMode,
required this.onToggleTheme,
required this.searchQuery,
required this.filteredLeaves,
required this.rootNodes,
required this.onSearchChanged,
required this.onOpenNode,
required this.onLogout,
});
final StudioUser user;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final String searchQuery;
final List<StudioNavNode> filteredLeaves;
final List<StudioNavNode> rootNodes;
final ValueChanged<String> onSearchChanged;
final ValueChanged<StudioNavNode> onOpenNode;
final VoidCallback onLogout;
@override
Widget build(BuildContext context) {
return Container(
color: _isDarkTheme(context) ? const Color(0xFF0A1320) : Colors.white,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: _SidebarHero(
user: user,
themeMode: themeMode,
onToggleTheme: onToggleTheme,
onLogout: onLogout,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextField(
onChanged: onSearchChanged,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search_rounded),
hintText: '搜索菜单或功能',
),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 16),
children: [
if (searchQuery.trim().isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
child: Text(
'搜索结果',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: _mutedColor(context),
),
),
),
...filteredLeaves.map(
(node) =>
_LeafNavTile(node: node, onTap: () => onOpenNode(node)),
),
if (filteredLeaves.isEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: Text('没有匹配的菜单项'),
),
] else ...[
_QuickLaunchStrip(
nodes: _allLeaves(rootNodes).take(4).toList(),
onOpenNode: onOpenNode,
),
const SizedBox(height: 12),
...rootNodes.map(
(node) => _TreeNode(node: node, onLeafTap: onOpenNode),
),
],
],
),
),
],
),
);
}
List<StudioNavNode> _allLeaves(List<StudioNavNode> nodes) {
final result = <StudioNavNode>[];
for (final node in nodes) {
if (node.children.isEmpty) {
result.add(node);
} else {
result.addAll(_allLeaves(node.children));
}
}
return result;
}
}
class _SidebarHero extends StatelessWidget {
const _SidebarHero({
required this.user,
required this.themeMode,
required this.onToggleTheme,
required this.onLogout,
});
final StudioUser user;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final VoidCallback onLogout;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
colors: [
scheme.primary.withOpacity(0.18),
scheme.tertiary.withOpacity(0.10),
_isDarkTheme(context) ? const Color(0xFF101B2D) : Colors.white,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(color: scheme.outlineVariant.withOpacity(0.7)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: scheme.primary.withOpacity(0.16),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.dashboard_customize_rounded,
color: scheme.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'TTX Studio',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
Text(
'Desktop Shell',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.60)),
),
],
),
),
],
),
const SizedBox(height: 16),
Text(
user.displayName,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 4),
Text(
'${user.workspace} · ${user.role}',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_MiniBadge(text: user.languageCode.toUpperCase()),
if (user.backendAppName.isNotEmpty)
_MiniBadge(text: user.backendAppName),
if (user.organizationName.isNotEmpty)
_MiniBadge(text: user.organizationName),
const _MiniBadge(text: 'Windows'),
const _MiniBadge(text: 'Backend'),
],
),
const SizedBox(height: 14),
FilledButton.tonalIcon(
onPressed: onLogout,
icon: const Icon(Icons.logout_rounded),
label: const Text('退出登录'),
),
const SizedBox(height: 8),
_ThemeToggleButton(themeMode: themeMode, onPressed: onToggleTheme),
],
),
);
}
}
class _QuickLaunchStrip extends StatelessWidget {
const _QuickLaunchStrip({required this.nodes, required this.onOpenNode});
final List<StudioNavNode> nodes;
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text(
'快速入口',
style: Theme.of(context).textTheme.titleSmall
?.copyWith(color: _mutedColor(context)),
),
),
...nodes.map(
(node) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _LeafNavTile(node: node, onTap: () => onOpenNode(node)),
),
),
],
);
}
}
class _TreeNode extends StatelessWidget {
const _TreeNode({required this.node, required this.onLeafTap});
final StudioNavNode node;
final ValueChanged<StudioNavNode> onLeafTap;
@override
Widget build(BuildContext context) {
if (node.children.isEmpty) {
return _LeafNavTile(node: node, onTap: () => onLeafTap(node));
}
return ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
childrenPadding: const EdgeInsets.only(left: 14, right: 2, bottom: 6),
leading: CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.primaryContainer
.withOpacity(_isDarkTheme(context) ? 0.16 : 0.55),
child: Icon(node.icon, size: 18),
),
title: Text(node.title),
subtitle: Text(node.summary),
children: node.children
.map((child) => _TreeNode(node: child, onLeafTap: onLeafTap))
.toList(),
);
}
}
class _LeafNavTile extends StatelessWidget {
const _LeafNavTile({required this.node, required this.onTap});
final StudioNavNode node;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
leading: CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.primaryContainer
.withOpacity(_isDarkTheme(context) ? 0.16 : 0.55),
child: Icon(node.icon, size: 18),
),
title: Text(node.title),
subtitle: Text(
node.summary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: onTap,
),
);
}
}
+354
View File
@@ -0,0 +1,354 @@
part of 'studio_app.dart';
class StudioWorkspaceView extends StatelessWidget {
const StudioWorkspaceView({
super.key,
required this.runtime,
required this.user,
required this.selectedTab,
required this.tabs,
required this.activities,
required this.rootNodes,
required this.refreshToken,
required this.onSelectTab,
required this.onCloseTab,
required this.onRefresh,
required this.onLogout,
required this.themeMode,
required this.onToggleTheme,
required this.onPageAction,
required this.onOpenNode,
});
final StudioRuntime runtime;
final StudioUser user;
final StudioTab selectedTab;
final List<StudioTab> tabs;
final List<StudioActivity> activities;
final List<StudioNavNode> rootNodes;
final int refreshToken;
final ValueChanged<String> onSelectTab;
final ValueChanged<String> onCloseTab;
final VoidCallback onRefresh;
final VoidCallback onLogout;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final void Function(String title, String detail) onPageAction;
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_WorkspaceHeader(
user: user,
selectedTab: selectedTab,
onRefresh: onRefresh,
onLogout: onLogout,
themeMode: themeMode,
onToggleTheme: onToggleTheme,
),
const SizedBox(height: 14),
_TabStrip(
tabs: tabs,
selectedTabId: selectedTab.id,
onSelectTab: onSelectTab,
onCloseTab: onCloseTab,
),
const SizedBox(height: 14),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 260),
child: _buildPage(
key: ValueKey('${selectedTab.id}:$refreshToken'),
tab: selectedTab,
activities: activities,
rootNodes: rootNodes,
runtime: runtime,
onPageAction: onPageAction,
onOpenNode: onOpenNode,
),
),
),
const SizedBox(height: 14),
_StudioStatusBar(
user: user,
selectedTab: selectedTab,
tabsCount: tabs.length,
activitiesCount: activities.length,
),
],
),
);
}
Widget _buildPage({
required Key key,
required StudioTab tab,
required List<StudioActivity> activities,
required List<StudioNavNode> rootNodes,
required StudioRuntime runtime,
required void Function(String title, String detail) onPageAction,
required ValueChanged<StudioNavNode> onOpenNode,
}) {
switch (tab.pageType) {
case StudioPageType.dashboard:
return StudioDashboardPage(
key: key,
tab: tab,
user: user,
activities: activities,
rootNodes: rootNodes,
onOpenNode: onOpenNode,
);
case StudioPageType.history:
return StudioHistoryPage(
key: key,
tab: tab,
runtime: runtime,
onAction: onPageAction,
);
case StudioPageType.sync:
case StudioPageType.repair:
return StudioOperationsPage(
key: key,
tab: tab,
runtime: runtime,
onAction: onPageAction,
);
case StudioPageType.sqlTemplate:
case StudioPageType.sqlRunner:
return StudioSqlPage(
key: key,
tab: tab,
runtime: runtime,
onAction: onPageAction,
);
case StudioPageType.portalMaintain:
return StudioPortalMaintainPage(
key: key,
tab: tab,
runtime: runtime,
onAction: onPageAction,
);
default:
return StudioFormEditorPage(
key: key,
tab: tab,
runtime: runtime,
onAction: onPageAction,
);
}
}
}
class _WorkspaceHeader extends StatelessWidget {
const _WorkspaceHeader({
required this.user,
required this.selectedTab,
required this.onRefresh,
required this.onLogout,
required this.themeMode,
required this.onToggleTheme,
});
final StudioUser user;
final StudioTab selectedTab;
final VoidCallback onRefresh;
final VoidCallback onLogout;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedTab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
selectedTab.summary,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
_MiniBadge(text: user.workspace),
_MiniBadge(text: user.displayName),
if (user.backendAppName.isNotEmpty)
_MiniBadge(text: user.backendAppName),
if (user.organizationName.isNotEmpty)
_MiniBadge(text: user.organizationName),
_MiniBadge(text: selectedTab.pageType.name),
],
),
],
),
),
const SizedBox(width: 16),
Wrap(
spacing: 10,
runSpacing: 10,
alignment: WrapAlignment.end,
children: [
_ThemeToggleButton(
themeMode: themeMode,
onPressed: onToggleTheme,
),
FilledButton.tonalIcon(
onPressed: onRefresh,
icon: const Icon(Icons.refresh_rounded),
label: const Text('刷新'),
),
FilledButton.tonalIcon(
onPressed: onLogout,
icon: const Icon(Icons.logout_rounded),
label: const Text('退出'),
),
],
),
],
),
),
);
}
}
class _TabStrip extends StatelessWidget {
const _TabStrip({
required this.tabs,
required this.selectedTabId,
required this.onSelectTab,
required this.onCloseTab,
});
final List<StudioTab> tabs;
final String selectedTabId;
final ValueChanged<String> onSelectTab;
final ValueChanged<String> onCloseTab;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 58,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: tabs.length,
separatorBuilder: (_, __) => const SizedBox(width: 10),
itemBuilder: (context, index) {
final tab = tabs[index];
final selected = tab.id == selectedTabId;
final scheme = Theme.of(context).colorScheme;
return InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () => onSelectTab(tab.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
width: 210,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: selected
? scheme.primary.withOpacity(0.18)
: _softPanelColor(context),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: selected
? scheme.primary.withOpacity(0.55)
: scheme.outlineVariant.withOpacity(0.8),
),
),
child: Row(
children: [
Icon(tab.icon, size: 18),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
tab.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
tab.pageType.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.62)),
),
],
),
),
if (tab.id != StudioTab.dashboardId)
IconButton(
tooltip: '关闭',
onPressed: () => onCloseTab(tab.id),
icon: const Icon(Icons.close_rounded, size: 16),
),
],
),
),
);
},
),
);
}
}
class _StudioStatusBar extends StatelessWidget {
const _StudioStatusBar({
required this.user,
required this.selectedTab,
required this.tabsCount,
required this.activitiesCount,
});
final StudioUser user;
final StudioTab selectedTab;
final int tabsCount;
final int activitiesCount;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
child: Wrap(
spacing: 16,
runSpacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
_MiniBadge(text: '用户 ${user.displayName}'),
_MiniBadge(text: '工作区 ${user.workspace}'),
if (user.backendAppName.isNotEmpty)
_MiniBadge(text: '应用 ${user.backendAppName}'),
if (user.organizationName.isNotEmpty)
_MiniBadge(text: '机构 ${user.organizationName}'),
_MiniBadge(text: '当前页 ${selectedTab.title}'),
_MiniBadge(text: '标签 ${tabsCount}'),
_MiniBadge(text: '活动 ${activitiesCount}'),
],
),
),
);
}
}
+135
View File
@@ -0,0 +1,135 @@
part of 'studio_app.dart';
bool _isDarkTheme(BuildContext context) {
return Theme.of(context).brightness == Brightness.dark;
}
Color _mutedColor(BuildContext context, [double opacity = 0.68]) {
return Theme.of(context).colorScheme.onSurface.withOpacity(opacity);
}
Color _softPanelColor(BuildContext context) {
return _isDarkTheme(context)
? const Color(0xFF0A1320)
: const Color(0xFFF1F6FA);
}
List<Color> _loginGradientColors(BuildContext context) {
return _isDarkTheme(context)
? const [Color(0xFF060B13), Color(0xFF0D1726), Color(0xFF07121D)]
: const [Color(0xFFF8FBFF), Color(0xFFEFF8F7), Color(0xFFF6F8FB)];
}
List<Color> _shellGradientColors(BuildContext context) {
return _isDarkTheme(context)
? const [Color(0xFF07111A), Color(0xFF091623), Color(0xFF07111A)]
: const [Color(0xFFF8FBFF), Color(0xFFEFF6FF), Color(0xFFF7FAFC)];
}
ThemeData buildStudioTheme(Brightness brightness) {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1AA6A6),
brightness: brightness,
);
final isDark = brightness == Brightness.dark;
final inputFill = isDark
? const Color(0xFF0E1726)
: colorScheme.surfaceContainerHighest;
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: colorScheme,
scaffoldBackgroundColor: isDark
? const Color(0xFF07101A)
: const Color(0xFFF6F8FB),
appBarTheme: AppBarTheme(
centerTitle: false,
backgroundColor: isDark ? const Color(0xFF07111A) : const Color(0xFFF8FBFF),
foregroundColor: colorScheme.onSurface,
elevation: 0,
),
cardTheme: CardThemeData(
color: isDark ? const Color(0xFF101B2D) : Colors.white,
elevation: isDark ? 0 : 1,
surfaceTintColor: colorScheme.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
drawerTheme: DrawerThemeData(
backgroundColor: isDark ? const Color(0xFF07111A) : Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: inputFill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: colorScheme.primary),
),
),
);
}
class _MiniBadge extends StatelessWidget {
const _MiniBadge({required this.text});
final String text;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _isDarkTheme(context)
? Colors.white.withOpacity(0.06)
: scheme.primaryContainer.withOpacity(0.45),
borderRadius: BorderRadius.circular(999),
),
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.78)),
),
);
}
}
class _ThemeToggleButton extends StatelessWidget {
const _ThemeToggleButton({required this.themeMode, required this.onPressed});
final ThemeMode themeMode;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final isDark = themeMode == ThemeMode.dark;
return FilledButton.tonalIcon(
onPressed: onPressed,
icon: Icon(isDark ? Icons.light_mode_rounded : Icons.dark_mode_rounded),
label: Text(isDark ? '亮色主题' : '暗色主题'),
);
}
}
class _GlowCircle extends StatelessWidget {
const _GlowCircle({required this.color});
final Color color;
@override
Widget build(BuildContext context) {
return Container(
width: 240,
height: 240,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
);
}
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
View File
@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
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 "ttx_page_admin")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.ttx_page_admin")
# 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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# 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()
+88
View File
@@ -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}
)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
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}
"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 preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+6
View File
@@ -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);
}
+148
View File
@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#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)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// 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, "ttx_page_admin");
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, "ttx_page_admin");
}
gtk_window_set_default_size(window, 1280, 720);
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);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(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() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
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_
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

Some files were not shown because too many files have changed in this diff Show More